var TA = TA || {};

(function (ns) {

   /*************
    ** PRIVATE **
   *************/

   const _WEBSITE_ID = 'd78574f9-20c3-4dcc-8d8d-85cf5b7ac141';   /* Custom Portal website id */

   /**
    * @function _runAction
    * @description:
    *  Executes a table-bound action through an asynchronous request
    * @param {string} actionId:
    *  Unique id of the action to execute
    * @param {object} recordReference:
    *  Object that represents the record on which the validating action will apply.
    *  This specifies ...
    *   entitySetName: The name of the entity set (e.g. contacts)
    *   recordId: The record id
    * @param {object} inputParams:
    *  Optional object containing any input parameters to pass to the action
    */
    function _runAction(actionId, recordReference, inputParams = {}) {

      console.info([
         'Action execution parameters:',
         `actionId = ${actionId}`,
         `recordReference = ${JSON.stringify(recordReference)}`,
         `inputParams = ${JSON.stringify(inputParams)}` 
      ].join('\n'));

      return new Promise(function (resolve) {
         webapi.safeAjax({
            type: 'POST',
            url: `/_api/${recordReference.entitySetName}(${recordReference.recordId})/Microsoft.Dynamics.CRM.${actionId}`,
            contentType: 'application/json',
            data: JSON.stringify(inputParams),
            /* Successful execution */
            success: function () {
               const resolution = { actionId: actionId, message: null };
               resolve(resolution);
               console.info({ 'Action execution successful': resolution });
            },
            /* Unsuccessful execution. Include response message in resolved object */
            error: function (res) {
               const message = res.responseJSON ? res.responseJSON.error.innererror.message : 'Web API not available';
               const resolution = { actionId: actionId, message: message };
               resolve(resolution);
               console.info({ 'Action execution not successful': resolution });
            }
         });
      });
   }

    /**
     * _runWorkflow
     * @description:
     *  Executes a classic workflow through an asynchronous request
     * @param {string} workflowId:
     *  Unique id of workflow to execute
     * @param {object} entityReference:
     *  Object containing record type (i.e. table logical name) and record id
     */
    function _runWorkflow(workflowId, entityReference) {

        // Parameters for this execution
        console.info([
            'Workflow execution parameters:',
            'workflowId = "' + workflowId + '"',
            'entityReference = ' + JSON.stringify(entityReference)
        ].join('\r\n'));

        return new Promise(function (resolve) {

            if (!entityReference || !entityReference.id) {
                console.log("No Id. Cannot run workflow. Returning empty json");
                resolve({}); //No need to run the workflow since the ID is not valid or is empty.
                //Passing a token value.
            }
            else {
                // https://debajmecrm.com/execute-a-workflow-programmatically-from-powerapps-portals-dynamics-365-workflows/
                shell.ajaxSafePost({
                    type: 'POST',
                    contentType: 'application/json',
                    url: '/_services/execute-workflow/' + _WEBSITE_ID,
                    data: JSON.stringify({
                        workflow: {
                            logicalName: 'workflow',
                            id: workflowId
                        },
                        entity: {
                            logicalName: entityReference.name,
                            id: entityReference.id
                        }
                    })
                }).then(

                    // Successful execution
                    function () {
                        let resolution = { workflowId: workflowId };
                        resolve(resolution);
                        console.info({ 'Workflow execution successful': resolution });
                    },

                    // Unsuccessful execution. Include response message in resolved object
                    function (error) {
                        let resolution = { workflowId: workflowId, message: error.responseJSON.Message };
                        resolve(resolution);
                        console.info({ 'Workflow execution not successful': resolution });
                    }

                );
            }
        });
    }

    /* Temporary storage reset/re-initialised on every page load */
    const _pageStorage = {};

    /************
     ** PUBLIC **
     ************/

    ns.Portal = {
        Utility: {
            Selector: {
                appendSelector: function (id) {
                    return "#" + id;
                },
                appendLabel: function (id) {
                    return id + "_label";
                },
                appendDescription: function (id) {
                    return id + "_description";
                },
                getByControlId: function (id) {
                    return $(this.appendSelector(id));
                },
                getTextLabel: function (id) {
                    return $(this.appendSelector(id) + "_label");
                },
                getLookupName: function (id) {
                    return $(this.appendSelector(id) + "_name");
                },
                getLookupEntity: function (id) {
                    return $(this.appendSelector(id) + "_entityname");
                },
                getDateTimeControl: function (id) {
                    return $("input[aria-labelledby='" + this.appendLabel(id) + "']");
                }
            },
            Validation: {
                required: function (id) {
                    console.log("Validation.required -> id: " + id);
                    var value = TA.Portal.Form.getValue(id);
                    if (value == null || (typeof value === 'string' && value.trim() == "") || (value.hasOwnProperty("id") && (value.id == "" || value.id == null))) {
                        return false;
                    } else {
					
                        return true;
                    }
                }
            },
            Event: {
                wireUp: function (events) {
                    console.log("Event.wireUp -> events: " + events);
                    for (var i in events) {
                        var e = events[i];
                        if (e.hasOwnProperty("t") && e.hasOwnProperty("f")) {
                            console.log("Event wireup -> c: " + e.c + ", t: " + e.t + ", f: " + e.f);
                            switch (e.t) {
                                case TA.Portal.EventType.OnChange:
                                    TA.Portal.Form.attachOnChange(e.c, e.f);
                                    break;
                                case TA.Portal.EventType.OnClick:
                                    alert("OnClick is not implemented");
                                    break;
                                case TA.Portal.EventType.OnKeypress:
                                    TA.Portal.Form.attachOnKeypress(e.c, e.f);
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                }
            },
            /**
             * Change default upload button to be consistent across different browsers. It is changing only attach file; not add notes.
             */
            changeUploadButton: function () {
                $("#AttachFile").before("<label id='attachFileLabel' for='AttachFile' style='padding-right:15px;padding-left:15px;margin-right:5px;border:solid 1px #ccc;cursor:pointer;'><i class='fa fa-cloud-upload' /> Browse</label>");
                $("#AttachFile").before("<span style='display:block;font-style;italic'><i>To attach multiple files, press Ctrl and select the files you wish to attach.</i></span>");
                $("#AttachFile").hide();
            },
            /**
             * Enable mouse click on lookup textbox. When a user clicks on a lookup control, it will display lookup dialog.
             */
            enableLookupOneClick: function () {
                $(".entity-form").on("click", ".lookup.form-control", function () {
                    $(this).parent().find(".launchentitylookup").trigger("click");
                });
            }
        },
        Page: {
            getParameterByName: function (name) {
                var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
                return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
            },
            getURLPram: function (param) {
                var results = new RegExp('[\?&]' + param + '=([^&#]*)')
                    .exec(window.location.search);
                return (results !== null && results.length > 1) ? (results[1] || '') : '';
            },
            getRecords: function (fetchQuery, attributes) {
                console.log("%cGetrecords reomved! please alert Taumata Arowai.", "font-size: 2rem; background-color: red; color: white");
                return new Promise((res, rej) => rej());
            },
            getRecords_v2: function (fetchQuery, attributes) {
                console.log("%cGetrecords_v2 reomved! please alert Taumata Arowai.", "font-size: 2rem; background-color: red; color: white");
                return new Promise((res, rej) => rej());
            },
            getRecords_v3: function (fetchQuery, p1, p2, p3, p4, p5) {
               return new Promise((resolve, reject) => {
                  $.ajax({
                     method: 'POST',
                     url: '/custom-search-v3/',
                     data: {
                        query: fetchQuery,
                        p1: p1,
                        p2: p2,
                        p3: p3,
                        p4: p4,
                        p5: p5,
                     }
                  }).done(function (msg) {
                     try { msg = JSON.parse(msg); } catch (e) { resolve(false); return; }
                     console.log({ msg: msg });
                     if (msg.error) {
                        console.error(msg.error);
                        resolve(false);
                        return;
                     }
                     resolve(msg.results);
                  }).fail(function (jqXHR, textStatus, errorThrown) {
                     console.log({ jqXHR: jqXHR, textStatus: textStatus, errorThrown: errorThrown });
                     reject(textStatus);
                  });
               });
            },
            setSectionVisible: function (id, flag, markMandatory) {
                if (flag) {
                    $("table[data-name='" + id + "']").parent().show();
                } else {
                    $("table[data-name='" + id + "']").parent().hide();
                }
                var required = (flag && markMandatory);
                $("table[data-name='" + id + "'] div.control").children().each(function () {
                    TA.Portal.Form.setFieldRequired(this.id, required);
                });
            },
            removeSection: function (id) {
                $("table[data-name='" + id + "']").parent().remove();
            },
            hasControl: function (id) {
                return TA.Portal.Utility.Selector.getByControlId(id).length > 0;
            }
        },
        Form: {
            s: function () {
                return TA.Portal.Utility.Selector;
            },
            isReadOnly: $("#EntityFormPanel").first().children().first().attr("readonly") != null,
            attachOnChange: function (id, callback) {
                console.log("attachOnChange -> id: " + id);
                var e = this.s().getByControlId(id);
                if (e.length > 0) {
                    console.log("attachOnChange -> control: " + e);
                    e.change(callback);
                    e.trigger("change");
                }
            },
            attachOnKeypress: function (id, callback) {
                console.log("attachOnKeypress -> id: " + id);
                var e = this.s().getByControlId(id);
                if (e.length > 0) {
                    console.log("attachOnKeypress -> control: " + e);
                    e.keypress(callback);
                    e.trigger("keypress");
                }
            },
            removeNextButton: function () {
                $("#NextButton").remove();
            },
            setControlDisabled: function (id, flag) {
                var ctrl = this.s().getByControlId(id);
                var ctrlType = this.getControlType(ctrl);

                if (ctrlType == this.controlType.DatetimePicker) {
                    this.s().getTextLabel(id).prop('disabled', flag);
                } else if (ctrlType == this.controlType.Checkbox) {
                    ctrl.prop('disabled', flag);
                } else if (ctrlType == this.controlType.Lookup) {
                    var l = this.s().getLookupName(id);
                    l.prop('disabled', flag);
                    l.siblings('div.input-group-btn').toggle(!flag);

                } else if (ctrlType == this.controlType.Radio) {
                    ctrl.children().prop("disabled", flag);
                } else if (ctrlType == this.controlType.Control) {
                    ctrl.prop('disabled', flag);
                }
            },
            setValue: function (id, value, text, entity) {
                var ctrl = typeof id == "object" ? $(id) : this.s().getByControlId(id);
                var fId = ctrl.attr("id");
                var ctrlType = this.getControlType(ctrl);

                if (ctrlType == this.controlType.Radio) {
                    if (value != null)
                        return ctrl.children(this.s().appendSelector(id) + "_" + (+value)).attr("checked", true);
                    else
                        ctrl.children().attr("checked", false);
                } else if (ctrlType == this.controlType.Checkbox) {
                    return ctrl.prop("checked", value);
                } else if (ctrlType == this.controlType.Lookup) {
                    if (value != null && value.hasOwnProperty('id') && value.hasOwnProperty('name') && value.hasOwnProperty('logicalname')) {
                        ctrl.val(value.id);
                        this.s().getLookupName(fId).val(value.name);
                        this.s().getLookupEntity(fId).val(value.logicalname);
                    } else {
                        ctrl.val(value);
                        this.s().getLookupName(fId).val(text);
                        this.s().getLookupEntity(fId).val(entity);
                    }
                } else if (ctrlType == this.controlType.DatetimePicker) {
                    ctrl.val(value);
                    return TA.Portal.Utility.Selector.getDateTimeControl(id).val(value);
                } else {
                    return ctrl.val(value);
                }
            },
            setPlaceholder: function (id, message) {
                console.log("setPlaceholder -> id : " + id);
                var c = this.s().getByControlId(id);
                var ctrlType = this.getControlType(c);

                var fid = this.s().appendSelector(id);
                console.log("fid " + fid);
                $(fid).attr("placeholder", message);
                $(fid).addClass('holder');
            },
            getValue: function (id) {
                console.log("getValue -> id : " + id);
                var ctrl = typeof id == "object" ? $(id) : this.s().getByControlId(id);
                console.log("getValue -> ctrl: " + ctrl);
                var ctrlType = this.getControlType(ctrl);
                console.log("getValue -> ctrlType: " + ctrlType);

                var value;

                if (ctrlType == this.controlType.Radio) {
                    value = ctrl.children(":checked").length > 0 ? ctrl.children(":checked").val() : ctrl.children().children(":checked").val();
                } else if (ctrlType == this.controlType.Checkbox) {
                    value = ctrl.prop("checked");
                } else if (ctrlType == this.controlType.Lookup) {
                    value = {
                        id: ctrl.val(),
                        name: this.s().getLookupName(id).val(),
                        logicalname: this.s().getLookupEntity(id).val()
                    };
                } else {
                    value = ctrl.val();
                }

                console.log("getValue -> returnValue: " + value);
                return value;
            },
            /**
             * Show or hide an attribute. If mandatory flag is present, it will make an attribute mandatory.
             * @param {string} id - Schema name of attribute.
             * @param {bool} flag - False for hide (default). True for show.
             * @param {bool} isMandatory - False for optional (default). True for making an attribute mandatory.
             */
            setControlVisible: function (id, flag, isMandatory) {
                console.log("setControlVisible -> id : " + id);
                var c = this.s().getByControlId(id);
                var ctrlType = this.getControlType(c);

                this.setFieldRequired(id, isMandatory);

                if (flag) {
                    if (ctrlType == this.controlType.Checkbox) {
                        c.parent().parent().parent().parent().show();
                    } else if (ctrlType == this.controlType.Lookup) {
                        c.parent().parent().parent().show(); //c.hasClass("money")
                    } else if (c.hasClass("money")) {
                        c.parent().parent().parent().parent().show();
                    } else {
                        c.parent().parent().show();
                    }
                } else {
                    if (ctrlType == this.controlType.Checkbox) {
                        c.parent().parent().parent().parent().hide();
                    } else if (ctrlType == this.controlType.Lookup) {
                        c.parent().parent().parent().hide();
                    } else if (c.hasClass("money")) {
                        c.parent().parent().parent().parent().hide();
                    } else {
                        c.parent().parent().hide();
                    }
                }
            },
            /**
             * Get the visiblity an attribute.
             * @param {string} id - Schema name of attribute.
             */
            getControlVisible: function (id) {
                console.log("getControlVisible -> id : " + id);
                var c = this.s().getByControlId(id);
                var ctrlType = this.getControlType(c);

                if (ctrlType == this.controlType.Checkbox) {
                    return c.parent().parent().parent().parent().is(':visible');
                } else if (ctrlType == this.controlType.Lookup) {
                    return c.parent().parent().parent().is(':visible');
                } else {
                    return c.parent().parent().is(':visible');
                }
            },
            /**
             * Make an attribute required. Mandatory is handled by default. Provide validationFunction and customMessage if it is a customised validation.
             * @param {string} id - Schema name of attribute.
             * @param {bool} flag - False for optional (default). True for mandatory.
             * @param {requestCallback} validationFunction - Custom validation function.
             * @param {string} customMessage - Custom message to display if the validation fails.
             */
            setFieldRequired: function (id, flag, validationFunction, customMessage) {
                var c = this.s().getByControlId(id);
                if (c.length > 0) {
                    var ctrlType = this.getControlType(c);

                    var g;

                    if (ctrlType == this.controlType.DatetimePicker) {
                        g = c.parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Lookup || ctrlType == this.controlType.Checkbox) {
                        g = c.parent().parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Radio) {
                        g = c.parent().siblings(".info");
                    } else if (c.hasClass("money")) {
                        g = c.parent().parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Control) {
                        g = c.parent().siblings(".info");
                    }

                    if (flag) {
                        $(g).attr("class", "info required");
                        this.setValidation(id, validationFunction, customMessage);
                    } else {
                        $(g).attr("class", "info");
                        this.removeValidation(id);
                    }
                }
            },
            /**
             * Show or hide a subgrid
             * @param {string} id - Name of subgrid.
             * @param {bool} flag - False for hide (default). True for show.
             */
            setSubgridVisible: function (id, flag) {
                console.log("setGridVisible -> id : " + id);
                var c = $("#" + id)
                if (flag) {
                    c.parent().parent().show();
                } else {
                    c.parent().parent().hide();
                }
            },
            setMultiSelectControlVisibility: function(id, isVisiblie, isMandatory){
                let control = $('#' +id);
                let controlLabel = control.closest('td').children('div.info');
                let controlLabelText = controlLabel.children('label')[0].innerHTML;

                if(isMandatory){
                    controlLabel.addClass('required');
                    this.setValidation(id, isVisiblie,  controlLabelText + ' is a required field');
                }
                else{
                    controlLabel.removeClass('required');
                    this.removeValidation(id);
                }
            },
            /**
             * Removes the page_validator entry based on its ID
             * @param {string} id - Element Id which is used as the unique id of the page validator.
             * @param {string} elementSuffix - Additional way to uniquely identify validators for the same element. Fields with multiple validators will be prevented(form metadata or programatically) from being overwritten.
             */
            removeValidation: function (id, elementSuffix) {
                var l = this.s().appendLabel(id);
                var vid = elementSuffix == null ? l : (l + "_" + elementSuffix);
                vid += "_Xrm_Client_Validation";
                //var vid = l + "_Xrm_Client_Validation";
                if (typeof Page_Validators != typeof undefined) {
                    Page_Validators = $.grep(Page_Validators, function (e) { return $(e).attr('id') != vid; });
                }
            },
            /**
             * Create a new page_validator entry for field validation
             * @param {string} id - Element Id which is used as the unique id of the page validator.
             * @param {method} validationFunction - Actual function which holds some logic that evaluates when to throw an error or not (returns true or false).
             * @param {string} customMessage - Error message to be displayed in the form.
             * @param {string} validationGroup - Particularly used for "Profile" page because it's a special case. But can be useful for evaluating related fields in any form.
             * @param {string} elementSuffix - Additional way to uniquely identify validators for the same element. Fields with multiple validators will be prevented(form metadata or programatically) from being overwritten.
             */
            setValidation: function (id, validationFunction, customMessage, validationGroup, elementSuffix) {
                console.log("set validation for id : " + id);
                var l = this.s().appendLabel(id);
                var c = this.s().getByControlId(id);
                var vid = elementSuffix == null ? l : (l + "_" + elementSuffix);
                vid += "_Xrm_Client_Validation";

                if (c.length > 0) {
                    var ctrlType = this.getControlType(c);

                    var g;

                    if (ctrlType == this.controlType.DatetimePicker) {
                        g = c.parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Lookup || ctrlType == this.controlType.Checkbox) {
                        g = c.parent().parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Radio) {
                        g = c.parent().siblings(".info");
                    } else if (c.hasClass("money")) {
                        g = c.parent().parent().siblings(".info");
                    } else if (ctrlType == this.controlType.Control) {
                        g = c.parent().siblings(".info");
                    }

                    var vG = validationGroup == null ? "" : validationGroup;

                    if (typeof Page_Validators != typeof undefined) {
                        Page_Validators = $.grep(Page_Validators, function (e) { return $(e).attr('id') != vid; });

                        var vF = validationFunction == null ? function () { return TA.Portal.Utility.Validation.required(id) } : validationFunction;

                        //retrive lable if there is no custom message
                        var m = customMessage == null ? $(g).children("label").html() + " is a required field." : customMessage;

                        if (typeof (Page_Validators) == 'undefined') return;
                        // Create new validator
                        var nv = document.createElement('span');
                        nv.style.display = "none";
                        nv.id = vid;
                        nv.controltovalidate = id;
                        nv.errormessage = "<a href='#" + l + "'>" + m + "</a>";
                        nv.validationGroup = vG;
                        nv.initialvalue = "";
                        nv.evaluationfunction = vF;

                        // Add the new validator to the page validators array:
                        Page_Validators.push(nv);

                        // Wire-up the click event handler of the validation summary link
                        $("a[href='#" + l + "']").on("click", function () { scrollToAndFocus("'" + l + "'", "'" + id + "'"); });
                    }
                }
            },
            /**
             * @function addServerSideValidationOnSubmit
             * @description:
             *  Replaces the value of the "onclick" attribute of a submit/next button to enable triggering
             *  of async validation via actions together with any sync validators already on the page
             * @param {object} submitButton:
             *  The submit/next button element
             * @param {object} validationParams:
             *  An array of objects that represent the parameters of one or more validating actions to run.
             *  This specifies ...
             *   actionId:
             *    Unique id of the action to execute
             *   recordReference:
             *    Object that represents the record on which the validating action will apply
             *    This specifies ...
             *     entitySetName: The name of the entity set (e.g. contacts)
             *     recordId: The record id
             *   inputParams:
             *    Optional object containing any input parameters to pass to the action
             *   checkForInclusion:
             *    Optional function to run upon submission that determines if a validating process should be included in execution
             *
             *  Or specifies ... 
             *   workflowId:
             *    The unique id of the validating workflow to execute
             *   msgHref:
             *    Optional corresponding href value to set on any resulting validation message
             *   entityReference:
             *    Object that represents the record on which the validating workflows will apply
             *    This specifies ...
             *     name: The record type (i.e. table logical name)
             *     id: The record id
             *   checkForInclusion:
             *    Optional function to run upon submission that determines if a validating process should be included in execution
             * @param {string} validationSummaryId:
             *  The id of the validation summary element
             * @param {object} preSubmit:
             *  A function containing logic to execute before submitting the form
             */
            addServerSideValidationOnSubmit: function (submitButton, validationParams, validationSummaryId, preSubmit) {
               if (!validationParams || validationParams.length < 1) return;
               $(submitButton).removeAttr('onclick').on('click', function () {
                  TA.Portal.Form.validateServerSideOnSubmit(this, validationParams, validationSummaryId, preSubmit);
               });
            },
            /**
             * @function validateServerSideOnSubmit
             * @description:
             *  Triggers async validation via actions together with any sync validators already
             *  on the page. This is meant to handle a submit/next button "onclick" event
             * @param {object} submitButton:
             *  The submit/next button element
             * @param {object} validationParams:
             *  An array of objects that specify ...
             *   actionId:
             *    Unique id of the action to execute
             *   recordReference:
             *    Object that represents the record on which the validating action will apply
             *    This specifies ...
             *     entitySetName: The name of the entity set (e.g. contacts)
             *     recordId: The record id
             *   validationSummaryId:
             *    The id of the validation summary element
             *   inputParams:
             *    Optional object containing any input parameters to pass to the action
             *   checkForInclusion:
             *    Optional function to run upon submission that determines if a validating process should be included in execution
             * 
             *  or that specify ...
             *   workflowId:
             *    The unique id of the validating workflow to execute
             *   msgHref:
             *    Optional corresponding href value to set on any resulting validation message
             *   entityReference:
             *    Object that represents the record on which the validating workflows will apply
             *    This specifies ...
             *     name: The record type (i.e. table logical name)
             *     id: The record id
             *   checkForInclusion:
             *    Optional function to run upon submission that determines if a validating process should be included in execution
             * @param {string} validationSummaryId:
             *  The id of the validation summary element
             * @param {object} preSubmit:
             *  A function containing logic to execute before submitting the form
             */
            validateServerSideOnSubmit: async function (submitButton, validationParams, validationSummaryId, preSubmit) {
         
               console.info('validateServerSideOnSubmit(): Executing ...');

               /* Alter buttons to visually indicate processing (store previous submit button text for restoring) */
               submitButton = $(submitButton);
               const buttonReadyText = submitButton.attr('value');
               $('input[type="button"]').prop('disabled', true);
               submitButton.attr('value', 'Processing...');

               /* Prepare validation summary */
               const validationSummary = $(`#${validationSummaryId}`);
               if (validationSummary.find('h4').length < 1) {
                  validationSummary
                     .attr('tabindex', '0')
                     .removeAttr('role')
                     .append(`
                        <h4 class="validation-header" role="status">
                           <span role="presentation" class="fa fa-info-circle">&nbsp;</span>
                           The form could not be submitted for the following reasons:
                        </h4>
                        <ul role="presentation" />`.replace(/\s+(?=<)/gm, ''));
               } else {
                  validationSummary.find('ul').empty();
               }

               /* Prepare warning summary */
               let warningSummary = $('#WarningSummary');
               if (warningSummary.length < 1) {
                  warningSummary = $(
                     `<div id="WarningSummary" class="validation-summary alert alert-warning alert-block" role="alert">
                        <h4 class="validation-header" role="none">
                           <span role="presentation" class="fa fa-info-circle">&nbsp;</span>
                           There are warnings for this form. Please submit again to proceed:
                        </h4>
                        <ul role="presentation" />
                     </div>`.replace(/\s+(?=<)/gm, ''))
                     .insertAfter(validationSummary);
               }
               warningSummary.hide().find('ul').empty();

               /* Default page validation */
               let hasClientErrors = typeof Page_ClientValidate === 'function' ? !Page_ClientValidate() : false;
               console.info(`hasClientErrors: ${hasClientErrors}`);

               /* Filtered validation execution */
               validationParams = validationParams.filter(function (param) {
                  return !param.checkForInclusion || param.checkForInclusion();
               });
               const validatingProcesses = validationParams.map(function (param){
                  return param.hasOwnProperty('workflowId')
                     ? _runWorkflow(
                        param.workflowId,
                        param.entityReference)
                     : _runAction(
                        param.actionId,
                        param.recordReference,
                        param.inputParams);
               });

               /* Processing */
               Promise.all(validatingProcesses).then(function (validationResults) {

                  /* Flags */
                  let hasServerErrors = false;
                  let hasServerWarnings = false;

                  /* Callable submit logic to allow for pre-submit promises */ 
                  const submit = function () {
                     console.info('Submitting form ...');
                     clearIsDirty();
                     WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(submitButton.attr('name'), '', true, '', '', false, true));
                  };

                  validationResults.forEach(function (validationResult, validationIndex) {
                     
                     let message = null;
                     let severity = null;

                     /* Evaluate messages returned in the process results */
                     try {
                        ({ severity, message } = JSON.parse(validationResult.message));
                     } catch (e) {
                        message = validationResult.message;
                     }
                     console.info({ validationIndex: validationIndex, severity: severity, message: message });

                     /* Addition of messages to the appropriate summary */
                     if (message) {
                        const { msgHref } = validationParams[validationIndex];
                        const listItem = `<li>${msgHref ? `<a href="${msgHref}">${message}</a>`: message}</li>`
                        if (severity === 0) {
                           /* Warning message */
                           hasServerWarnings = hasServerWarnings || true;
                           warningSummary.find('ul').append(listItem);
                        } else {
                           /* Validation message */
                           hasServerErrors = hasServerErrors || true;
                           validationSummary.find('ul').append(listItem);
                        }
                     }
                     console.info(`hasServerErrors: ${hasServerErrors}, hasServerWarnings: ${hasServerWarnings}`);

                     /* On to next validation result */
                     if (validationIndex !== validationParams.length - 1) return;

                     /* Form submission on successful validation with either no warnings or bypassed warnings */
                     console.info(`bypassWarnings: ${_pageStorage.bypassWarnings}`);
                     if (!hasClientErrors && !hasServerErrors && (!hasServerWarnings || _pageStorage.bypassWarnings)) {
                        console.info('Validation successful');
                        try {
                           if (typeof preSubmit === 'function') {
                              console.info('Executing pre-submit function ...');
                              const preSubmitResult = preSubmit();
                              if (preSubmitResult instanceof Promise) {
                                 console.info('A promise was made on pre-submit. Awaiting settlement ...');
                                 preSubmitResult
                                    .then(function () {
                                       console.info('Promise fulfilled');
                                       submit();
                                       console.info('validateServerSideOnSubmit(): Execution completed');
                                    })
                                    .catch(function (error) {
                                       console.error(`validateServerSideOnSubmit(): Promise rejected\n${error ? `: ${error}` : ''}`);
                                    });
                                 return;
                              }
                           }
                           submit();
                           console.info('validateServerSideOnSubmit(): Execution completed');
                        } catch (error) {
                           console.error(`validateServerSideOnSubmit(): An error occurred\n${error ? `: ${error}` : ''}`);
                        }
                        return;
                     }

                     /* Set validation on next submit to bypass warnings when only warnings are presented */
                     if (hasServerWarnings) {
                        _pageStorage.bypassWarnings = !hasClientErrors && !hasServerErrors;
                        console.info(`bypassWarnings set to ${_pageStorage.bypassWarnings}`);
                     }

                     /* Ensure appropriate summaries are visible and scroll up to messages */
                     if (hasServerErrors) {
                        validationSummary.show();
                        $(window).scrollTop(0);
                     } else if (!hasClientErrors && hasServerWarnings) {
                        warningSummary.show();
                        $(window).scrollTop(0);
                     }

                     /* Restore button to a ready state */
                     $('input[type="button"]').prop('disabled', false);
                     submitButton.attr('value', buttonReadyText);
                  });
               });
            },
            showSelectedFiles: function () {
                var files = $(this)[0].files;
                $(this).parent().next().remove();
                if (files.length > 0) {
                    var dom = "<div class='control document'>File(s) selected to upload are<ul>";
                    for (var i = 0; i < files.length; i++) {
                        var file = files[i];
                        dom += "<li>" + file.name + "</li>";
                    }
                    dom += "</ul></div>";
                    console.log("files count: " + files.length);
                    console.log(dom);
                    $(this).parent().after(dom);
                }
            },
            getControlType: function (c) {
                console.log("getControlType -> c: " + c);
                if (c.length > 0) {
                    if (c.attr("data-ui") == "datetimepicker") {
                        return this.controlType.DatetimePicker;
                    } else if (c.attr("type") == "checkbox") {
                        return this.controlType.Checkbox;
                    } else if (c.attr("type") == "hidden") {
                        return this.controlType.Lookup;
                    } else if (c.attr("class") != null && (c.attr("class").indexOf("boolean-radio") > -1 || c.attr("class").indexOf("picklist horizontal") > -1 || c.attr("class").indexOf("picklist vertical") > -1)) {
                        return this.controlType.Radio;
                    } else {
                        return this.controlType.Control;
                    }
                }
            },
            controlType: {
                Control: 1,
                Lookup: 2,
                DatetimePicker: 3,
                Radio: 4,
                Checkbox: 5
            }
        },
        EventType: {
            OnChange: 1,
            OnClick: 2,
            OnKeypress: 3
        }
    };

    console.info('TA.Portal namespace initialised by web file');

})(TA);