fiftyoneDegreesManager = function() {
    'use-strict';
    var json = {{&_jsonObject}};
    var parameters = {{&_parameters}};
    var sessionId = "{{&_sessionId}}";
    var sessionKey = "{{_objName}}_" + sessionId;
    this.sessionId = sessionId;
    var sequence = {{&_sequence}};

    // Log any errors returned in the JSON object.
    if(json.error !== undefined){
        console.log(json.error);
    }

    // Log any warnings returned in the JSON object.
    if (json.warnings !== undefined) {
        console.log(json.warnings);
    }

    // Set to true when the JSON object is complete.
    var completed = false;

    // Set to true when the `catchError` is called.
    let failed = false;

    // changeFuncs is an array of functions. When onChange is called and passed
    // a function, the function is registered and is called when processing is
    // complete.
    var changeFuncs = [];

    // Counter is used to count how many pieces of callbacks are expected. Every
    // time the completedCallback method is called, the counter is decremented
    // by 1.
    var callbackCounter = 0;

    // Array of JavaScript properties that have started evaluation.
    var jsPropertiesStarted = [];

    // startsWith polyfill.
    var startsWith = function(source, searchValue) {
        return source.lastIndexOf(searchValue, 0) === 0;
    }

    // endsWith polyfill.
    var endsWith = function(source, searchValue) {
        return source.substring(source.length - searchValue.length, source.length) === searchValue;
    }

    var clearCache = function() {
        if (sessionStorage) {
            for (i = 0; i < sessionStorage.length; i++) {
                key = sessionStorage.key(i);
                if (startsWith(key, sessionKey)) {
                    sessionStorage.removeItem(key);
                }
            }
        }
    }

    var loadParameters = function() {
        if (sessionStorage) {
            var parametersString = sessionStorage.getItem(sessionKey + "_parameters");
            if (parametersString) {
                parameters = JSON.parse(parametersString);
            }
        }
        return parameters;
    }

    var saveParameters = function(sourceParams) {
        if (sourceParams) {
            parameters = sourceParams
        }

        if (sessionStorage) {
            var parametersString = JSON.stringify(parameters);
            sessionStorage.setItem(sessionKey + "_parameters", parametersString);
        }
    }

    // Get stored values with the '51D_' prefix that have been added to the request
    // and return the data as key value pairs. This method is needed to extract
    // stored values for inclusion in the GET or POST request for situations
    // where CORS will prevent them from being sent to third parties.
    var getFodSavedValues = function() {
        let fodValues = {};
        {{#_enableCookies}}
        {
            let keyValuePairs = document.cookie.split(/; */);
            for(let nextPair of keyValuePairs) {
                let firstEqualsLocation = nextPair.indexOf("=");
                let name = nextPair.substring(0, firstEqualsLocation);
                if(startsWith(name, "51D_")){
                    let value = nextPair.substring(firstEqualsLocation+1);
                    fodValues[name] = value;
                }
            }
        };
        {{/_enableCookies}}
        {{^_enableCookies}}
        {
            // Collect values from session storage
            let session51DataPrefix = sessionKey + "_data_";
            for(let i = 0, n = window.sessionStorage.length; i < n; ++i) {
                let nextKey = window.sessionStorage.key(i);
                if(startsWith(nextKey, session51DataPrefix)){
                    let value = window.sessionStorage[nextKey];
                    fodValues[nextKey.substring(session51DataPrefix.length)] = value;
                }
            }
        };
        {{/_enableCookies}}
        return fodValues;
    };

    // Extract key value pairs from the '51D_' prefixed values and concatenates
    // them to form a query string for the subsequent json refresh.
    var getParametersFromStorage = function() {
        var fodValues = getFodSavedValues();
        var keyValuePairs = [];
        for (var key in fodValues) {
            if (fodValues.hasOwnProperty(key)) {
                // Url encode the value.
                // This is done to ensure that invalid characters (e.g. = chars at the end of
                // base 64 encoded strings) reach the server intact.
                // The server will automatically decode the value before passing it into the
                // Pipeline API.
                keyValuePairs.push(key + "=" + encodeURIComponent(fodValues[key]));
            }
        }
        return keyValuePairs;
    };

    // Fetch a value safely from the json object. If a key somewhere down the
    // '.' separated hierarchy of keys is not present then 'undefined' is
    // returned rather than letting an exception occur.
    var getFromJson = function(key, allowObjects, allowBooleans) {
        var result = undefined;
        if (typeof allowObjects === "undefined") { allowObjects = false; }
        if (typeof allowBooleans === "undefined") { allowBooleans = false; }

        if (typeof(key) === "string") {
            var functions = json;
            var segments = key.split('.');
            var i = 0;
            while (functions !== undefined && i < segments.length) {
                functions = functions[segments[i++]];
            }
            if (typeof functions === "string") {
                result = functions;
            } else if (allowBooleans && typeof functions === "boolean") {
                result = functions;
            } else if (allowObjects && typeof functions === "object" && functions !== null) {
                result = functions;
            }
        }
        return result;
    }

    // Executed at the end of the processJSproperties method or for each piece
    // of JavaScript which has 51D code injected. When there are 0 pieces of
    // JavaScript left to process then reload the JSON object.
    var completedCallback = function(resolve, reject){
        callbackCounter--;
        if (callbackCounter === 0) {
{{#_updateEnabled}}
            processRequest(resolve, reject);
{{/_updateEnabled}}
        } else if (callbackCounter < 0){
            reject('Too many callbacks.');
        }
    }

    // Executes any JavaScript contained in the JSON data. Session storage is
    // used to check the process state of the JavaScript property, if the name
    // of the property exists as a key then it has been processed. If all the
    // processed JavaScript properties have been flagged as processed already
    // then session storage is checked again for a JSON payload. If it exists
    // then this is loaded into the managers internal data store. If not or if
    // JavaScript properties have been processed then the call-back is
    // processed with any new evidence produced by the JavaScript properties.
    // If JavaScript properties are processed then a key containing the name of
    // the JavaScript property is added to session storage. The complete flag is
    // set to true when there is no further JavaScript to be processed.
    var processJsProperties = function(resolve, reject, jsProperties, ignoreDelayFlag) {
        var executeCallback = true;
        var started = 0;
        var cached = 0;
        var toProcess = 0;

        // If there is no cached response and there are JavaScript code snippets
        // then process them and perform any call-backs required.
        if (jsProperties !== undefined && jsProperties.length > 0) {

            {{^_enableCookies}}
            let valueSetPrefix = new RegExp('document\\.cookie\\s*=\\s*(("([A-Za-z0-9_"\\s\\+]+)\\s*=\\s*"\\s*\\+\\s*([^\\s};]+))|(`([A-Za-z0-9_]+)\\s*=\\s*\\$\\{([^}]+)\\}`))', 'g');
            let session51DataPrefix = sessionKey + "_data_";
            let sessionSetPatch = 'window.sessionStorage["' + session51DataPrefix + '$3$6"]=$4$7';
            {{/_enableCookies}}

            // Execute each of the JavaScript property code snippets using the
            // index of the value to access the value to avoid problems with
            // JavaScript returning erroneous values.
            for (var index = 0; index < jsProperties.length; index++) {
                var name = jsProperties[index];
                if (jsPropertiesStarted.indexOf(name) !== -1) {
                    continue;
                }
                var body = getFromJson(name);

                // If there is a body then this property should be processed.
                if (body) {
                    toProcess++;
                }

                var isCached = sessionStorage && sessionStorage.getItem(sessionKey + "_property_" + name);

                // If the property has already been processed then skip it.
                if (isCached) {
                    cached++;
                    continue;
                }

                // Create new function bound to this instance and execute it.
                // This is needed to ensure the scope of the function is
                // associated with this instance if any members are altered or
                // added. Avoids global scoped variables.

                var delay = getFromJson(name + 'delayexecution', false, true);

                if (
                    (ignoreDelayFlag || delay === undefined || delay === false) &&
                    typeof body === "string" &&
                    body.length
                ) {
                    var func = undefined;
                    var searchString = '// 51D replace this comment with callback function.';
                    completed = false;
                    jsPropertiesStarted.push(name);
                    started++;

                    {{^_enableCookies}}
                    body = body.replaceAll(valueSetPrefix, sessionSetPatch);
                    {{/_enableCookies}}

                    if (body.indexOf(searchString) !== -1){
                        callbackCounter++;
                        body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);');
                        func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc',
                            "try {\n" +
                            body + "\n" +
                            "} catch (err) {\n" +
                            "console.log(err);" +
                            "}"
                        );
                        func(completedCallback, resolve, reject);
                        executeCallback = false;
                    } else {
                        func = new Function(
                            "try {\n" +
                            body + "\n" +
                            "} catch (err) {\n" +
                            "console.log(err);" +
                            "}"
                        );
                        func();
                    }

                    if (sessionStorage) {
                        sessionStorage.setItem(sessionKey + "_property_" + name, true)
                    }

                    // If the property is `javascripthardwareprofile` then check if the
                    // profile has been set. If not then remove current property from
                    // the list of properties that have started to prevent the
                    // 2nd request from being made.
                    if (name === "device.javascripthardwareprofile") {
                        var hrw = getFodSavedValues();
                        if (hrw && !hrw["51D_ProfileIds"]) {
                            // find and remove name from jsPropertiesStarted
                            var propIndex = jsPropertiesStarted.indexOf(name);
                            if (propIndex > -1) {
                                jsPropertiesStarted.splice(index, 1);
                            }
                            started--;
                            toProcess--;
                        }
                    }
                }
            }
        }

        if ((cached === toProcess || started === 0) && sessionStorage)  {
            var cachedResponse = sessionStorage.getItem(sessionKey);
            if (cachedResponse) {
                loadJSON(resolve, reject, cachedResponse);
                executeCallback = false;
            }
        }

        if (started === 0) {
            executeCallback = false;
            failed = false;
            completed = true;
        }

        if (executeCallback) {
            callbackCounter = 1;
            completedCallback(resolve, reject);
        }
    };

{{#_updateEnabled}}
{{^_supportsFetch}}
    // Standard method to create a CORS HTTP request ready to send data.
    var createCORSRequest = function(method, url) {
        var xhr;
        try {
            xhr = new XMLHttpRequest();
        } catch(err){
            xhr = null;
        }
        if (xhr !== null && "withCredentials" in xhr) {

            // Check if the XMLHttpRequest object has a "withCredentials"
            // property.
            // "withCredentials" only exists on XMLHTTPRequest2 objects.
            xhr.open(method, url, true);
        } else if (typeof XDomainRequest != "undefined") {

            // Otherwise, check if XDomainRequest.
            // XDomainRequest only exists in IE, and is IE's way of making CORS
            // requests.
            xhr = new XDomainRequest();
            xhr.open(method, url);
        } else {

            // Otherwise, CORS is not supported by the browser.
            xhr = null;
        }
        return xhr;
    };
{{/_supportsFetch}}
{{/_updateEnabled}}

    // Check if the JSON object still has any JavaScript snippets to run.
    var hasJSFunctions = function() {
        for (var i = i; i < json.javascriptProperties; i++) {
            var body = getFromJson(json.javascriptProperties[i]);
            if (body !== undefined && body.length > 0) {
                return true;
                }
        }
        return false;
    }

    // Process the JavaScript properties.
    var process = function(resolve, reject){
        processJsProperties(resolve, reject, json.javascriptProperties, false);
    }

    var fireChangeFuncs = function(json) {
        for (var i = 0; i < changeFuncs.length; i++) {
            if (typeof changeFuncs[i] === 'function' &&
                changeFuncs[i].length === 1) {
                changeFuncs[i](json);
            }
        }
    }

{{#_updateEnabled}}
    // Process the response as json and call the resolve method.
    var loadJSON = function(resolve, reject, responseText) {
        try {
            json = JSON.parse(responseText);
        } catch(err) {
            clearCache();
            reject(new Error("Invalid JSON - the endpoint is likely setup incorrectly", { cause: err }));
            return;
        }

        if (hasJSFunctions()) {
            // json updated so fire 'on change' functions
            // before executing any new JS properties that
            // have come back.
            fireChangeFuncs(json);
            process(resolve, reject);
        } else {
            failed = false;
            completed = true;
            // json updated so fire 'on change' functions
            // This must happen after completed = true in order
            // for 'complete' functions to fire.
            fireChangeFuncs(json);
            resolve(json);
        }
    }

    // Sends a POST request to the call-back URL to retrieve and updated
    // JSON payload from the cloud service. A POST request is used so that
    // parameters can be passed in the request body, this is to get around
    // the limitations on the length of query strings. As POST requests are not
    // cached by browsers, the result of the POST request is stored in session
    // storage on a successful response. This can then be checked before making
    // repeat requests to the call-back URL.
    // Any saved value parameters that have been set by the executed JavaScript
    // properties are added to the list of parameters, this list is then
    // serialized as Form Data and sent in the POST body to the call-back URL,
    // refreshing the JSON data. The new JSON is then loaded if the request is
    // returned with a success status code. If there was a problem then the
    // session storage items are invalidated and reject is called.
    var processRequest = function(resolve, reject){
        loadParameters();

        // Get additional parameters in case they are not sent
        // by the browser.
		var savedValueParams = getParametersFromStorage();
        for(var savedValueIndex in savedValueParams) {
            var parts = savedValueParams[savedValueIndex].split('=');
            parameters[parts[0]] = parts[1];
        }

        saveParameters();

        var params = [];
        for (var param in parameters) {
            if (parameters.hasOwnProperty(param)) {
                params.push(param+"="+parameters[param])
            }
        }

        // Add the session and sequence to the request
        if (sessionId) {
            params.push("session-id=" + sessionId);
        }
        if (sequence) {
            params.push("sequence=" + sequence);
        }

        var postBody = "";
        if (params.length > 0) {
            postBody = params.join('&').replace(/%20/g, '+');
        }

{{#_supportsFetch}}
        fetch('{{{_url}}}', {
            method: 'POST',
            mode: 'cors',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: postBody
        })
        .then(response => {
            return response.text();
        })
        .then(responseText => {
{{/_supportsFetch}}
{{^_supportsFetch}}
        // Request callback URL with additional parameters.
        var xhr = createCORSRequest('POST', '{{{_url}}}');

        // If there is no support for HTTP requests then call reject and throw
        // a no CORS support error.
        if (!xhr) {
            reject(new Error('CORS not supported'));
            return;
        }

        // Add the HTTP header for POST form data.
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.setRequestHeader('Accept', 'application/json');

        xhr.onload = function () {

            // Get the response body from the request.
            var responseText = xhr.responseText;

{{/_supportsFetch}}
            // Cache the response text.
            if (sessionStorage) {
                sessionStorage.setItem(sessionKey, responseText);
            }
            // Load the JSON object from the response text
            loadJSON(resolve, reject, responseText);
            // Increment the sequence on a successful request
            sequence++;
{{#_supportsFetch}}
        })
        .catch(error => {
            // Invalidate the cache on error.
            clearCache();
            reject(error);
        });
{{/_supportsFetch}}
{{^_supportsFetch}}
        };

        xhr.onerror = function () {
            // An error occurred with the request. Invalidate the cache and
            // return the details in the call to reject method.
            clearCache();
            reject(Error(xhr.statusText));
        };

        // Send the GET request.
        xhr.send(postBody);
{{/_supportsFetch}}
    }
{{/_updateEnabled}}

    // Function logs errors, used to 'reject' a promise or for error callbacks.
    var catchError = function(value) {
        failed = true;
		function errorToDesc(subError) {
			let msg = subError.message;
			if (!msg) {
				msg = String(subError);
			}
			let cause = subError.cause;
			if (cause) {
				msg += '\n--- caused by ---\n';
				msg += errorToDesc(cause);
			}
			return msg;
		}
        let errorDesc = errorToDesc(value);
        console.log(errorDesc);

        if (json.errors === null || json.errors === undefined) {
            json.errors = [errorDesc];
        } else if (Array.isArray(json.errors)) {
            json.errors.push(errorDesc);
        }
        fireChangeFuncs(json);
    }

    // Populate this instance of the FOD object with getters to access the
	// properties. If the value is null then get the noValueMessage from the
	// JSON object corresponding to the property.
    var update = function(data){
        var self = this;
        Object.getOwnPropertyNames(data).forEach(function(key) {
            self[key] = {};
            for(var i in data[key]){
                var obj = self[key];
                (function(i) {
                    Object.defineProperty(obj, i, {
                        get: function (){
                            if(data[key][i] === null && (i !== "javascriptProperties")){
                                return data[key][i + "nullreason"];
                            } else {
                                return data[key][i];
                            }
                        }
                    })
                })(i);
            }
        });
    }

{{#_hasDelayedProperties}}
    // Get the JS property(s) that, when evaluated, will populate
    // evidence that can be used to determine the value of the
    // supplied property.
    // The supplied name can either be a complete property name or a top level
    // aspect name.
    // Where the aspect name is given, ALL evidence properties under that
    // key will be returned.
    // Example property names are 'location.country' or 'devices.profiles.hardwarename'
    // Example aspect names are 'location' or 'devices'
    var getEvidenceProperties = function (name) {
        var evidenceProperties = getFromJson(name + 'evidenceproperties');
        if(typeof evidenceProperties === "undefined") {
            var item = getFromJson(name, true);
            evidenceProperties = getEvidencePropertiesFromObject(item);
        }
        return evidenceProperties;
    }

    // Get all values in any 'evidenceproperty' fields on this object
    // or sub-objects.
    var getEvidencePropertiesFromObject = function (dataObject) {
        evidenceProperties = [];

        for (var prop in dataObject) {
            if (dataObject.hasOwnProperty(prop)) {
                var value = dataObject[prop];
                // Property name ends with 'evidenceproperties' so is
                // what we're looking for.
                // Add the values to the array if we don't already have it.
                if (value !== null && Array.isArray(value) && endsWith(prop, 'evidenceproperties')) {
                    value.forEach(function(item, index) {
                        if(evidenceProperties.indexOf(item) === -1) {
                            evidenceProperties.push(item);
                        }
                    });
                }
                // Item is an object so recursively call this method
                // and add any resulting evidence properties to the list.
                else if(typeof value === 'object' && value !== null) {
                    getEvidencePropertiesFromObject(value).forEach(function(item, index) {
                        if(evidenceProperties.indexOf(item) === -1) {
                            evidenceProperties.push(item);
                        }
                    });
                }
            }
        }

        return evidenceProperties;
    }
{{/_hasDelayedProperties}}

{{#_supportsPromises}}
    this.promise = new Promise(function(resolve, reject) {
        process(resolve,reject);
    });
{{/_supportsPromises}}

	this.onChange = function(resolve) {
		changeFuncs.push(resolve);
	}

    this.complete = function(resolve, properties) {
{{#_hasDelayedProperties}}
        // If properties is set then check if we need to kick off
        // processing of anything.
        if(typeof properties !== "undefined") {
            // If properties is a string then split on comma to produce
            // an array of one or more key names.
            if(typeof properties === "string") {
                properties = properties.split(',');
            }
            if(Array.isArray(properties)) {
                properties.forEach(function(key, i) {
                    // We pass an empty function rather than 'resolve' because we
                    // don't want to call resolve when a single evidence function
                    // evaluates but after all of them have completed.
                    // This is handled by the 'if(complete)' code below.
                    processJsProperties(function(json) {}, catchError, getEvidenceProperties(key), true);
                });
            }
        }

{{/_hasDelayedProperties}}
        if(completed || failed){
            resolve(json);
        }else{
            this.onChange(function(data) {
                if(completed || failed){
                    resolve(data);
                }
            })
        }
    };

    // Update this instance with the initial JSON payload.
    update.call(this, json);
{{#_supportsPromises}}
    var parent = this;
    this.promise.then(function(value) {
        // JSON has been updated so replace the current instance.
        update.call(parent, value);
        failed = false;
        completed = true;
    }).catch(catchError);
{{/_supportsPromises}}
{{^_supportsPromises}}
    process(function(json) {}, catchError);
{{/_supportsPromises}}
}

var {{_objName}} = new fiftyoneDegreesManager();
