affirm s2.json /* we need this as our basis */;
affirm s2.json.stringify /* was added later, might not be in all (two?) trees yet (ha!) */;
/**
   This object basically acts as a (mostly) drop-in replacement for
   s2.json, and it uses s2.json to implement most of its
   functionality. It uses a custom, script-side stringify() which is
   orders of magnitude less efficient (on several levels) than
   s2.json.stringify() (which is implemented in C), but allows
   overriding of to-JSON behaviour on a per-container or per-prototype
   basis.
*/
return {
    /** See s2.json.parse(). */
    parse: s2.json.parse,
    /** See s2.json.parseFile(). */
    parseFile: s2.json.parseFile,
    /**
       Converts the value v into a JSON string (or throws while trying).

       indention may be either a falsy value (for no intenation), a
       string (which gets prepending N times for N levels of
       indentation), or an integer: a positive value indents that many
       spaces and a negative value indents that many tabs.

       v need not be a root-level value (Object or Array), but may be
       a string, number, or boolean.

       Returns a string on success, throws on error.

       Notes about special cases:

       - If v or a prototype of v contains a function property named
       toJSON() then v.toJSON() is used in place of v for
       to-JSON-string conversion. The function must return some
       JSON-able form of v. e.g. an implementation for a Hashtable
       might return an Object in the form {keys:[...], values:[...]}.

       - Object _keys_ which are _not_ of type (string, integer, double)
       are elided from the output. Keys of numeric types are converted
       to strings for JSON key purposes.

       - Objects elide any keys which have value counterpart of
       undefined. JSON does not know 'undefined'. We "could" translate
       it to null here, but we instead opt to elide it.

       - The undefined value: if passed to this function, the string
       'null' is returned. undefined is also translated to 'null' in
       the context of array empty entries.
    */
    stringify: proc stringify(v, indention = stringify.config.indention){
        affirm ++stringify.level > 0;
        const ex = catch{
            s2.isDerefable(v) && s2.isCallable(v.toJSON) && (v = v.toJSON());
            const f = tmap # typename v;
            affirm f /* Argument must be a known JSON-able type or have a toJSON() method. */;
            if('string'===typename f){
                affirm --stringify.level>=0;
                return f;
            }else if(!f.buffered){
                /* "Simple" conversions which do not recurse */
                const rc = f(v);
                affirm --stringify.level>=0;
                return rc;
            }else if(stringify.level>stringify.config.maxOutputDepth){
                throw exception('CWAL_RC_RANGE',
                                "Output depth limit ("+stringify.config.maxOutputDepth+
                                ") exceeded while generating JSON.");
            }else{
                affirm f.buffered /* f.buffered is set, so... */;
                const jbuf = s2.Buffer.new() /* gets appended to by f() */;
                f(v) /* appends all output to jbuf */;
                affirm --stringify.level>=0;
                affirm !jbuf.isEmpty();
                return jbuf.takeString();
            }
        };
        affirm --stringify.level>=0;
        assert ex /* or we couldn't have gotten this far */;
        throw ex;
    }.withThis(proc(){
        /**
           Public configuration for stringify(). Change these
           options to modify the defaults.
        */
        this.config = {
            /* Default indention used by stringify(). */
            indention: undefined,
            /* Separator for entries in arrays and object lists. */
            commaSeparator: ', ',
            /* Separator for keys and value in objects. */
            keyValSeparator: ': ',
            /* Max object/array depth to allow before erroring
               out. Remember that cycles will generally be detected
               before this happens, so this doesn't necessarily
               indicate that any cycles were encountered.
            */
            maxOutputDepth: 15
        };
        this.level = 0;
        return this;
    }).importSymbols({
        // some crazy scoping and var accesses going on here...
        /**
           Indents the output, if appropriate, based on the current
           call level (or the level specified by the 2nd
           parameter). If addNL is true, a newline is appended before
           the indentation. This is a no-op if stringify() is called
           with a falsy indention parameter.
         */
        indent: proc callee(addNL=true, level = stringify.level){
            indention || return;
            callee.idbuf || (callee.idbuf = s2.Buffer.new(64));
            if(callee.prevLevel !== level){
                callee.prevLevel = level;
                if('integer'===typename indention){
                    const len = (indention<0) ? -indention : indention;
                    affirm len >= 0;
                    callee.idbuf.length( len * level )
                        .fill((indention<0) ? 0x09 : 0x20);
                }else if('string' === typename indention){
                    callee.idbuf.reset();
                    for(var i = 0; i < level; ++i){
                        callee.idbuf << indention;
                    }
                }
            }
            addNL && (jbuf << '\n');
            jbuf << callee.idbuf;
        },
        /**
           A hashtable mapping typenames to either strings (for static
           conversions) or a function taking a value parameter. Those
           functions normally return a string, but if the function has
           a 'buffered' property which is truthy then its return
           result is ignored and instead a Buffer value named jbuf is
           made available to them, and they are expected to append all
           output there.
         */
        tmap: scope {
            const proxy4Obj = proc(v){
                v.mayIterate() || throw "Cycles detected.";
                jbuf << '{';
                proxyEachProp.first = true;
                //proxyEachProp('LEVEL', stringify.level);
                const ex = catch v.eachProperty(proxyEachProp);
                indention && indent(true,stringify.level-1);
                jbuf << '}';
                ex && throw ex;
            }.importSymbols({
                // Object.eachProperty() proxy.
                proxyEachProp: proc callee(k,v){
                    undefined === v && return;
                    callee.first
                        ? callee.first = false
                        : jbuf << stringify.config.commaSeparator;
                    indention && indent();
                    const tk = typename k;
                    if('string'===tk){
                        jbuf << k.toJSONString();
                    }else if('integer'===tk||'double'===tk){
                        jbuf << '"' << k << '"';
                    }else{
                        return;
                    }
                    jbuf << stringify.config.keyValSeparator
                        << stringify(v,indention);
                }
            });
            const proxy4Array = proc(v){
                v.mayIterate() || throw "Cycles detected.";
                jbuf << '[';
                proxyEachIndex.first = true;
                v.eachIndex(proxyEachIndex);
                indention && indent(true,stringify.level-1);
                jbuf << ']';
            }.importSymbols({
                proxyEachIndex: proc callee(v){
                    callee.first
                        ? callee.first = false
                        : jbuf << stringify.config.commaSeparator;
                    indention && indent();
                    jbuf << stringify(v,indention);
                }
            });

            proxy4Obj.buffered =
                proxy4Array.buffered = true
            /* tells stringify() to set up a buffer to send the
               results to. */;
            const nativeImpl = s2.json.stringify;
            const h = s2.Hash.new(17);
            h.insert('array', proxy4Array);
            h.insert('bool', nativeImpl);
            h.insert('double', nativeImpl);
            h.insert('exception', proxy4Obj);
            h.insert('integer', nativeImpl);
            h.insert('null', 'null');
            h.insert('object', proxy4Obj);
            h.insert('string', nativeImpl);
            h.insert('undefined', 'null');
            h; // scope result
        }
    })/*stringify()*/
};
