ko.expressionRewriting = (() => {
    var
/*
        javaScriptReservedWords = ["true", "false", "null", "undefined"],

    // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
    // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
    // This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911).
        javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i,

        getWriteableValue = expression => {
            if (javaScriptReservedWords.includes(expression))
                return false;
            var match = expression.match(javaScriptAssignmentTarget);
            return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
        },
*/
    // The following regular expressions will be used to split an object-literal string into tokens

        specials = ',"\'`{}()/:[\\]',    // These characters have special meaning to the parser and must not appear in the middle of a token, except as part of a string.
        // Create the actual regular expression by or-ing the following regex strings. The order is important.
        bindingToken = RegExp([
            // These match strings, either with double quotes, single quotes, or backticks
            '"(?:\\\\.|[^"])*"',
            "'(?:\\\\.|[^'])*'",
            "`(?:\\\\.|[^`])*`",
            // Match C style comments
            "/\\*(?:[^*]|\\*+[^*/])*\\*+/",
            // Match C++ style comments
            "//.*\n",
            // Match a regular expression (text enclosed by slashes), but will also match sets of divisions
            // as a regular expression (this is handled by the parsing loop below).
            '/(?:\\\\.|[^/])+/w*',
            // Match text (at least two characters) that does not contain any of the above special characters,
            // although some of the special characters are allowed to start it (all but the colon and comma).
            // The text can contain spaces, but leading or trailing spaces are skipped.
            '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
            // Match any non-space character not matched already. This will match colons and commas, since they're
            // not matched by "everyThingElse", but will also match any other single character that wasn't already
            // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
            '[^\\s]'
        ].join('|'), 'g'),

        // Match end of previous token to determine whether a slash is a division or regex.
        divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
        keywordRegexLookBehind = {'in':1,'return':1,'typeof':1},

        parseObjectLiteral = objectLiteralString => {
            // Trim leading and trailing spaces from the string
            var str = ko.utils.stringTrim(objectLiteralString);

            // Trim braces '{' surrounding the whole object literal
            if (str.charCodeAt(0) === 123) str = str.slice(1, -1);

            // Add a newline to correctly match a C++ style comment at the end of the string and
            // add a comma so that we don't need a separate code block to deal with the last item
            str += "\n,";

            // Split into tokens
            var result = [], toks = str.match(bindingToken), key, values = [], depth = 0;

            if (toks.length > 1) {
                var i = 0, tok;
                while ((tok = toks[i++])) {
                    var c = tok.charCodeAt(0);
                    // A comma signals the end of a key/value pair if depth is zero
                    if (c === 44) { // ","
                        if (depth <= 0) {
                            result.push((key && values.length)
                                ? {key: key, value: values.join('')}
                                : {'unknown': key || values.join('')});
                            key = depth = 0;
                            values = [];
                            continue;
                        }
                    // Simply skip the colon that separates the name and value
                    } else if (c === 58) { // ":"
                        if (!depth && !key && values.length === 1) {
                            key = values.pop();
                            continue;
                        }
                    // Comments: skip them
                    } else if (c === 47 && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {  // "//" or "/*"
                        continue;
                    // A set of slashes is initially matched as a regular expression, but could be division
                    } else if (c === 47 && i && tok.length > 1) {  // "/"
                        // Look at the end of the previous token to determine if the slash is actually division
                        var match = toks[i-1].match(divisionLookBehind);
                        if (match && !keywordRegexLookBehind[match[0]]) {
                            // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
                            str = str.slice(str.indexOf(tok) + 1);
                            toks = str.match(bindingToken);
                            i = -1;
                            // Continue with just the slash
                            tok = '/';
                        }
                    // Increment depth for parentheses, braces, and brackets so that interior commas are ignored
                    } else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
                        ++depth;
                    } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
                        --depth;
                    // The key will be the first token; if it's a string, trim the quotes
                    } else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'"
                        tok = tok.slice(1, -1);
                    }
                    values.push(tok);
                }
                if (depth > 0) {
                    throw Error("Unbalanced parentheses, braces, or brackets");
                }
            }
            return result;
        },

        preProcessBindings = (bindingsStringOrKeyValueArray) => {

            var resultStrings = [],
//                propertyAccessorResultStrings = [],
                keyValueArray = parseObjectLiteral(bindingsStringOrKeyValueArray),

                processKeyValue = (key, val) => {
                    // Values are wrapped in a function so that each value can be accessed independently
                    val = 'function(){return ' + val + ' }';
                    resultStrings.push("'" + key + "':" + val);
                };

            keyValueArray.forEach(keyValue =>
                processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value)
            );
/*
            if (propertyAccessorResultStrings.length)
                processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
*/
            return resultStrings.join(",");
        };

    return {
        parseObjectLiteral: parseObjectLiteral,

        preProcessBindings: preProcessBindings,

        keyValueArrayContainsKey: (keyValueArray, key) =>
            -1 < keyValueArray.findIndex(v => v['key'] == key),

        // Internal, private KO utility for updating model properties from within bindings
        // property:            If the property being updated is (or might be) an observable, pass it here
        //                      If it turns out to be a writable observable, it will be written to directly
        // allBindings:         An object with a get method to retrieve bindings in the current execution context.
        //                      This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable
        // key:                 The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus'
        // value:               The value to be written
        // checkIfDifferent:    If true, and if the property being written is a writable observable, the value will only be written if
        //                      it is !== existing value on that writable observable
        writeValueToProperty: (property, allBindings, key, value, checkIfDifferent) => {
            if (!property || !ko.isObservable(property)) {
                throw Error(`${key} , must be observable`);
//                allBindings.get('_ko_property_writers')?.[key]?.(value);
            } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
                property(value);
            }
        }
    };
})();
