/**
   Demonstrates a basic script module loading API similar to
   require.js (http://requirejs.org).

   In short, this module acts as a loader for arbitrary other content,
   be it s2 script code, raw file content, or "virtual" (non-file)
   content plugged in by the user. Its usage is modelled very much of
   off requirejs, and it will look very familiar to anyone who has
   used that library.

   See the API docs in this module for more details. For even more
   details (or other details, in any case), see this public Google
   Doc:

   https://docs.google.com/document/d/14gRP4f-WWgWNS64KM_BI7YjQqBLl4WT3jIrmNmFefYU/view

   Example usage:

   Assume the files r1.s2 and r2.s2 live somewhere in this module's
   search path and that both return an Object with a foo() method.
   This is how we might import and use them:

   @code
   const R = s2.import('require.s2');

   R(['r1', 'r2'], proc(r1, r2){
     r1.counter = r2.counter = 1;
     print(__FLC,r1);
     print(__FLC,r2);
     r1.foo();
     r2.foo();
     ++r1.counter;
     ++r2.counter;
   });

   R(['r2', 'r1'], proc(r2, r1){
     assert 2 === r2.counter;
     assert 2 === r1.counter;
     // ^^^ because imported script results are cached
   });
   @endcode

*/
affirm s2.io;
affirm 'string'===typename s2.io.dirSeparator;
affirm 'function'===typename s2.io.realpath;
affirm 'function'===typename s2.Buffer.readFile;
affirm 'function'===typename s2.PathFinder.new;
scope {

    const importFileBuffer = s2.Buffer.readFile;

    const importFileText = proc(fn){
        return importFileBuffer(fn).takeString();
    }.importSymbols(nameof importFileBuffer);

    /**
       Internal utility to convert almost-URL-encoded plugin options
       to some value form. Treats non-existent values (===undefined)
       as a boolean true flag.
    */
    const convertQValue = proc(v){
        undefined === v && return true;
        'true' === v && return true;
        'false' === v && return false;
        var x = 0.parseNumber(v);
        undefined!==x && return x;
        return v;
    };


    /**
       This "default" plugin (A) acts as the default
       implementation for fetching require()'d files, (B) is
       where the interface for other plugins is documented.
    */
    var PluginModel ={
        /** If true, require() does no search for the filename it
            is given. (It is assumed to be some form of virtual
            unique identifier, and may be empty.)
        */
        isVirtual: false,
        
        /**
           cacheIt tells require whether or not to cache the results
           of requests to this plugin. If it is true, then this.load()
           will only be called for the first request, and its result
           will be returned on subsequent requests. EXCEPTION: if
           a plugin is called with arguments (see load()) the cache
           is ignored/bypassed.

           The default is expected (by client code) to always be true
           for the default plugin!
        */
        cacheIt: true,

        /**
           For file-based plugins, prefix specifies the search
           directories (array of strings, excluding any trailing slash
           on the directory parts unless they refer to the root
           directory).
        */
        prefix: ('string' === typename var tmp = s2.getenv('S2_REQUIRE_PATH'))
            ? tmp.split(tmp.indexOf(';') >= 0 ? ';' : ':') : ['.'],
        
        /**
           For file-based plugins, suffix specifies the search
           extensions (array of strings, including any '.' part,
           e.g. use ".foo" instead of "foo" unless you're searching
           for partial extensions).
        */
        suffix: ('string' === typename tmp = s2.getenv('S2_REQUIRE_EXTENSIONS'))
            ? tmp.split(tmp.indexOf(';') >= 0 ? ';' : ':') : ['.s2'],

        /**
           Called by require() to "load" a given "file."  load() must
           accept a file name, "load" that file, for a given
           definition of "load", and return its contents or
           result. (It need not load anything from anywhere, much less
           a file.)

           In the call context, 'this' will be this plugin
           configuration object.

           The first argument is the "name" part of the string passed
           to require.

           The second argument part is either undefined or an object:
           if the file string contains '?', anything after the ? is
           assumed to be encoded in the form a=b&c=d... (URL-like, but
           _without_ the URL encoding), and that gets transformed into
           an Object of key/value pairs before passing them to this
           function. HOWEVER: passing any arguments after '?' will
           cause caching to be bypassed, because the arguments presumably
           change how the plugin works.
           
           Caveats: 1) all of the values in the opt object will
           originally be of type string, but numeric-looking strings
           get converted to numbers.  2) Only one level of object is
           supported, not nested values, arrays, etc.
        */
        load: proc(fn){
            return s2.import(fn);
        }.importSymbols(nameof s2)
        /* Reminder to self: we cannot simply alias to s2.import() because
           any extra non-string arguments would be passed to it (doing the
           wrong thing). Reminder #2: adding comments outside of function
           bodies, instead of if them, uses less memory, since they don't
           get allocated as part of the function body. That's especially
           significant when the length of the comments outweigh the rest
           of the source, as in this case. */
        ;
    };

    /**
       The main "require" module object.
    */
    var mod = {
        __typename: 'Requirerer',
        /**
           Used for doing file lookups. Its search path and extensions
           get continually swapped out by require() and friends.
        */
        pf: s2.PathFinder.new()
    };

    /**
      The funkiness we do with 'this' vs 'mod' in may placed places
      below is so that this stuff still works when clients copy the
      returned module into a property of another object. i.e. the following
      usages are equivalent:

      var x = mod;
      obj.x = mod;
      x([...],...);
      obj.x([...],...);

      Both calls (because of these extra bindings) use the same "this"
      inside the call - the local 'mod' symbol, which is important for
      identical/correct semantics in both uses.
    */

    /**
       All the plugins are stored here, and any number may potentially
       be loaded (and added here) via client-side use.
    */
    mod.plugins = {
        /**
           The name 'default' is magic and assumes certain
           plugin-level defaults. It also provides the default .prefix
           and .suffix properties for other non-isVirtual (file-using)
           plugins (used only if those plugin does not specify them).
        */
        default: PluginModel,
        /**
           Works just like the default plugin but bypasses
           the cache.
        */
        nocache: {
            cacheIt: false,
            load: PluginModel.load
        },
        /** Loads file content as a string. */
        text: {
            cacheIt: false,
            // prefix: uses the defaults
            suffix: ['.txt', '.s2', '.html'],
            load: importFileText
        },
        /** Loads file content as a buffer. */
        buffer: {
            cacheIt: false,
            // prefix: uses the defaults
            suffix: ['.txt', '.s2', '.html'],
            load: importFileBuffer
        }

        /**
           Demonstration of a "virtual" plugin (one which does not
           use files).
           
           virtualDemo:{
               isVirtual: true,
               cacheIt: true, // not strictly necessary, plugin-dependent
               load: proc f(fn,opt){
               print("Example of a 'virtual' handler. Arguments:",argv);
                   return opt ||| this;
               }
           }
        */

    };

    /**
       Adds a new plugin. name must be a string and pluginObj must
       be an object which follows the PluginModel interface.
    */
    mod.addPlugin = proc(name, pluginObj){
        affirm 'string' === typename name;
        affirm pluginObj && pluginObj.load && pluginObj.load.importSymbols /* assume it's a function */;
        mod.plugins[name] = pluginObj;
        return mod;
    }.importSymbols(nameof mod);

    /**
       Searches for a plugin script by using this.plugins.default's
       search path and the name ("plugins/"+name). Returns undefined
       if not found, else the result of s2.import()'ing that file. It
       does not check if the plugin is already installed, but installs
       (or overwrites) it into mod.plugins[name].
    */
    mod.searchAndInstallPlugin = proc(name){
        mod.pf.prefix = mod.plugins.default.prefix;
        mod.pf.suffix = mod.plugins.default.suffix;
        var fn = mod.pf.search('plugins/'+name);
        return fn
            ? mod.plugins[name] = ((const requireS2=mod), s2.import(s2.io.realpath(fn)))
            : undefined;
    }.importSymbols(nameof mod),

    /**
       If the given plugin name is already installed, it is returned,
       otherwise it is sought for, installed, and returned. An
       exception is thrown if it cannot be found or if installing it
       fails.
    */
    mod.getPlugin = proc(name){
        //print(__FLC,'plugin',name, mod.plugins[name]);
        return mod.plugins[name] ||| mod.searchAndInstallPlugin(name);
    }.importSymbols(nameof mod),

    /**
       Given a base filename, it searches for the file using the
       configured search paths/extensions (from this.plugins). If
       found, it is passed to the import() function specified
       for the import type (see below).

       By default this.plugins.default is used to search for and
       import the file. If a "special" type of name is provided to
       this function, though (meaning the base name looks like
       with "SOMETHING!basename"), then this.plugins[SOMETHING] is
       used (if set), which may change the caching behaviour and
       how the content of the file is interpreted.

       Depending on the configuration options, requests might get
       cached. Subsequent calls which expand to the same file name
       will return the same (cached) result value on each
       subsequent calls.
    */
    mod.import = function /*callee*/(basename){
        affirm 'string' === typename basename;
        var pluginName;
        /* Check for pluginName!... */
        if((var ndx = basename.indexOf('!'))>0){
            pluginName = basename.substr(0,ndx);
            basename = basename.substr(ndx+1);
        }
        pluginName || (pluginName ='default');
        /* Configuration for this plugin... */
        const requireS2 = mod /* public API symbol, potentially
                                 needed by anything which
                                 uses anyPlugin.load() */;
        var pConf = mod.getPlugin(pluginName);
        //affirm pConf.load inherits callee.prototype /* is-a Function */;

        const pf = pConf.isVirtual ? undefined : mod.pf /* PathFinder */;
        if(pf){
            // Set up file lookup paths/suffixes...
            pf.prefix = pConf.prefix ||| mod.plugins.default.prefix;
            pf.suffix = pConf.suffix ||| mod.plugins.default.suffix;
        }
        /* Treat ?a=b&c=d... almost like a URL-encoded
           as query string, but without the URL encoding.
        */
        var qArgs, useCache = pConf.cacheIt;
        if(basename.indexOf('?')>=0){
            /* Parse args. If any are provided, bypass the cache. */
            var sp = basename.split('?',2);
            basename = sp.0;
            (qArgs = splitQArgs(sp.1))
                && useCache && (useCache = false);
        }
        /* Find the file, if necessary... */
        var fn = pConf.isVirtual
            ? basename
            : (basename ? pf.search( basename ) : false)
            ||| (throw "Cannot find '%1$s' in search path %2$J with extensions %3$J.".
                 applyFormat(basename, pf.prefix, pf.suffix))
        /* s2 "pro tip": the extra parens around the throw are a
           tokenization/evaluation optimization (requires less
           work for the evaluator than skipping over a non-grouped
           expression). String.concat() is also more efficient for
           more than two strings than using (string + string).
           Pro tip 2: comments in function bodies cost memory, as
           they are allocated along with the function body. There
           goes 39 more bytes.
        */;
        pConf.isVirtual || (fn = s2.io.realpath(fn));
        //print(__FLC,"fn=",fn,"useCache=",useCache,"qArgs =",qArgs,pluginName,pConf);
        //print(__FLC,'pConf.cacheIt=',pConf.cacheIt,', fn=',fn);
        //print(__FLC,cache.toJSONString(2));
        return useCache
            ? (((const key = pConf.isVirtual
                 ? pluginName
                 : (pluginName ? pluginName+'!'+fn : fn)),
                cache.hasOwnProperty(key))
               ? cache[key]
               : cache[key] = pConf.load(fn, qArgs))
        : pConf.load(fn, qArgs);
    }.importSymbols({
        mod: mod,
        cache: {},
        splitQArgs: proc(str){
            str || return str;
            var rc;
            str.split('&').eachIndex(proc(v){
                var sp = v.split('=',2);
                (rc ||| (rc = {}))[sp.0] = convertQValue(sp.1);
            });
            return rc;
        }.importSymbols(nameof convertQValue);
    });

    /**
       For each base filename in the given array, this function calls
       this.import(basename) and appends the results to a new array.
       Returns an array of the results to each import() call. Throws
       if list is not an array or is empty.
    */
    mod.importList = function(list){
        //print(__FLC,typename list, list);
        affirm list inherits argv.prototype /* is-a Array */;
        const imps = [], n = list.length();
        affirm n>0;
        // pre-reserving arrays can inadvertently bypass an
        // allocation optimization!
        //(n>6/*shameful abuse of cwal-internal magic number!*/)
        //    && imps.reserve(n);
        for( var i = 0; i < n; ++i ) imps.push(mod.import(list[i]));
        return /*__BREAKPOINT, */ imps;
    }.importSymbols(nameof mod);

    /**
       Imports a list of script modules and calls a callback after
       loading all of them.

       list is an array of strings - script file base names to import.
       func is a function which gets called after importing all of the
       scripts.  It is passed one argument for each entry in the list,
       in the same order they are imported. funcThis is the (optional)
       'this' for the function call.

       Example:

       assert 42 === thisObj(
         ['module1', 'module2'],
         function(mod1, mod2){
           print('result of module1 import:' mod1);
           print('result of module2 import:' mod2);
           return 42;
         }
       );

       ACHTUNG: The require() mechanism function "exports" the module
       loader instance into the call chain via the const symbol
       'requireS2', the intention being that imported modules can
       (because of s2's funky symbol lookup) use this symbolic name to
       recursively call into require() if needed (it is often useful
       to do so, it turns out, and this consolidates the convention
       across modules originally written for different code bases).


       _Potential_ TODOs:

       Alternate usages:

       - require('name', 'name', ..., function )

       - If passed a non-string value where a name is expected, use it
       as-is as the result of the loading. i suspect we might have
       some interesting uses for that, but want a use case before trying
       it out.

    */
    mod.require = function callee(list, func, funcThis = func){
        func && affirm func inherits callee.prototype /* is-a Function */;
        return func
            ? func.apply(funcThis, mod.importList(list))
            : mod.importList(list);
    }.importSymbols(nameof mod);


    if(var d = __FILEDIR){
        // Add this file's directory to some lookup paths.
        mod.plugins.default.prefix.push( mod.require.home
                                         = s2.io.realpath(d) );
    }else{
        /* maybe a synthetic __FILE name (e.g. via eval)? */
        mod.require.home = "";
    }
    /* We set mod.require.home so that plugins can construct paths via
       requireS2.home. */

    /* Make it so that call()ing this object calls mod.require() */
    mod.prototype = mod.require /** Holy cow! We've just inherited our
                                    own member function. */;
    mod;
}
