--------------------------------------------------------------------------------

branch: 'v01'
  - version: 1.x
  - DOES support closure
  - does NOT support sandbox

branch: 'v02'
  - version: 2.x
  - includes potentially breaking changes
    to add support for a sandbox

-------------------------------------------------------------------------------- v02: design

Java:
=====

boolean useJsClosure = script.useJsClosure();
boolean useJsSandbox = script.useJsSandbox();

JS (!useJsClosure):
==================

// userscript

JS (useJsClosure):
==================

(function(unsafeWindow, useSandbox){
  useSandbox = (useSandbox && unsafeWindow.Proxy && unsafeWindow.Reflect);

  // ====================================================
  // placeholder: define locally scoped GM.* and GM_* API
  // ====================================================
  var GM = GM_info  = {};
  var var GM_addElement = GM_addStyle = GM_deleteValue = GM_fetch = GM_getResourceText = GM_getResourceURL = GM_getValue = GM_listValues = GM_log = GM_registerMenuCommand = GM_setValue = GM_unregisterMenuCommand = GM_xmlhttpRequest = GM_addValueChangeListener = GM_download = GM_getTab = GM_getTabs = GM_notification = GM_openInTab = GM_removeValueChangeListener = GM_saveTab = GM_setClipboard = GM_webRequest = function(){};
  // ====================================================

  var _GM_getWindowProxyHander = function(windowOwnProps) {
    if (!windowOwnProps || (typeof windowOwnProps !== 'object')) {
      windowOwnProps = {};
    }

    var windowProxyHander = {

      defineProperty(target, prop, descriptor) {
        unsafeWindow.Object.defineProperty(windowOwnProps, prop, descriptor);
        return true;
      },

      deleteProperty(target, prop) {
        if (prop in windowOwnProps) {
          delete windowOwnProps[prop];
          return true;
        }
        return false;
      },

      get(target, prop, receiver) {
        var value, thisArg;
        if (prop in windowOwnProps) {
          value = windowOwnProps[prop];
          thisArg = windowOwnProps;
        }
        else {
          value = unsafeWindow.Reflect.get(target, prop);
          thisArg = target;
        }
        return (typeof value === 'function')
          ? value.bind(thisArg)
          : value;
      },

      getOwnPropertyDescriptor(target, prop) {
        if (prop in windowOwnProps) {
          return unsafeWindow.Object.getOwnPropertyDescriptor(windowOwnProps, prop);
        }
        return unsafeWindow.Reflect.getOwnPropertyDescriptor(target, prop);
      },

      has(target, prop) {
        return (prop in windowOwnProps) || (prop in target);
      },

      ownKeys(target) {
        return [
          ...unsafeWindow.Object.keys(windowOwnProps),
          ...unsafeWindow.Reflect.ownKeys(target)
        ];
      },

      set(target, prop, value, receiver) {
        windowOwnProps[prop] = value;
      },

    };

    return windowProxyHander;
  };

  var _GM_addApiToWindowProxy = function(windowProxy) {
    if (typeof windowProxy === 'object') {
      var entries = {

        // ---------------------
        // Greasemonkey API (v4)
        // ---------------------
        'GM': GM,

        // -------------------------
        // Greasemonkey API (legacy)
        // -------------------------
        'GM_addElement':                GM_addElement,
        'GM_addStyle':                  GM_addStyle,
        'GM_deleteValue':               GM_deleteValue,
        'GM_fetch':                     GM_fetch,
        'GM_getResourceText':           GM_getResourceText,
        'GM_getResourceURL':            GM_getResourceURL,
        'GM_getValue':                  GM_getValue,
        'GM_info':                      GM_info,
        'GM_listValues':                GM_listValues,
        'GM_log':                       GM_log,
        'GM_registerMenuCommand':       GM_registerMenuCommand,
        'GM_setValue':                  GM_setValue,
        'GM_unregisterMenuCommand':     GM_unregisterMenuCommand,
        'GM_xmlhttpRequest':            GM_xmlhttpRequest,

        // ------------------
        // JSMISSINGFUNCTIONS
        // ------------------
        'GM_addValueChangeListener':    GM_addValueChangeListener,
        'GM_download':                  GM_download,
        'GM_getTab':                    GM_getTab,
        'GM_getTabs':                   GM_getTabs,
        'GM_notification':              GM_notification,
        'GM_openInTab':                 GM_openInTab,
        'GM_removeValueChangeListener': GM_removeValueChangeListener,
        'GM_saveTab':                   GM_saveTab,
        'GM_setClipboard':              GM_setClipboard,
        'GM_webRequest':                GM_webRequest
      };

      var keys = Object.keys(entries);
      var key, val;
      for (var i=0; i < keys.length; i++) {
        key = keys[i];
        val = entries[key];
        windowProxy[key] = val;
      }
    }
  };

  var window, self, globalThis;
  if (useSandbox) {
    window = self = globalThis = new unsafeWindow.Proxy(unsafeWindow, _GM_getWindowProxyHander());
    window.window = window.self = window;
    _GM_addApiToWindowProxy(window);
  }
  else {
    window = self = globalThis = unsafeWindow;
  }

  var userscript_wrapper = function(){
    // userscript
  }

  userscript_wrapper.call(window)
})(window, true);  // where value of 2nd parameter is set by the Java variable: 'useJsSandbox'

-------------------------------------------------------------------------------- v02: development

update: Script model and DB schema

changes to userscript metadata block:
=====================================

* add properties:
    @flag noJsClosure
    @flag noJsSandbox

    @flags noJsClosure|noJsSandbox

changes to Script model:
========================

* add methods:
    script.useJsClosure()
    script.useJsSandbox()

* remove methods:
    script.isUnwrap()

changes to DB schema in ScriptStoreSQLite:
==========================================

ALTER TABLE 'script' RENAME COLUMN 'unwrap' TO 'flags';

this column is an INTEGER.

previously, it could only hold values: 0 or 1.
which correspond to the boolean presence or absence
of the (now legacy) metadata property: '@unwrap'
where its value (if any) doesn't matter.

the previous usage for the DB field is compatible with its new usage.
  static final int noJsClosureFlag = 1;
  static final int noJsSandboxFlag = 2;

where:
  @unwrap
is equivalent to:
  @flag noJsClosure

cross-reference to other implementations:
=========================================

----------------
common behavior:
----------------

* when there is NOT a sandbox:
  - 'unsafeWindow' is not defined
  - 'window' exposes the real 'window'

* when there IS a sandbox:
  - 'unsafeWindow' is defined and exposes the real 'window'
  - 'window' is a proxy that restricts access to the real 'window'

--------------
violentmonkey:
--------------

https://violentmonkey.github.io/api/metadata-block/#unwrap
  '@unwrap'
    does NOT use a closure
    does NOT use a sandbox
    does NOT include the GM.* or GM_* API
    note: '@grant' is ignored

https://violentmonkey.github.io/api/metadata-block/#grant
  '@grant none'
    DOES use a closure
    does NOT use a sandbox
    does NOT include the GM.* or GM_* API
  '@grant xxx'
    conditionally includes a cherry-picked GM.* or GM_* API method
    note: has the side-effect that a sandbox is enabled
  when no '@grant' is specified
    default: '@grant none'

-------------
tampermonkey:
-------------

https://www.tampermonkey.net/documentation.php#meta:unwrap
  '@unwrap'
    does NOT use a closure
    does NOT use a sandbox
    note: documentation is unclear regarding GM_* API and '@grant'
    test: confirms that behavior is identical to 'violentmonkey'

https://www.tampermonkey.net/documentation.php#meta:grant
  '@grant none'
    DOES use a closure
    does NOT use a sandbox
    does NOT include the GM.* or GM_* API
  '@grant xxx'
    conditionally includes a cherry-picked GM.* or GM_* API method
  when no '@grant' is specified
    DOES use a closure
    DOES use a sandbox
    does NOT include the GM.* or GM_* API

usage of flags in my implementation:
====================================

* both a closure and a sandbox ARE used by default,
  unless explicitly disabled
* to explicitly disable a closure,
  use any of:
    '@unwrap'
    '@flag noJsClosure'
    '@flags noJsClosure'
* a sandbox cannot be used without a closure
* to explicitly disable a sandbox,
  either explicitly disable a closure,
  or use any of:
    '@grant none'
    '@flag noJsSandbox'
    '@flags noJsSandbox'

---------
behavior:
---------

* when there is NOT a closure:
  - there is NOT a sandbox
  - 'unsafeWindow' is not defined
  - the real 'window' is exposed
    by the following globally scoped variables:
    * 'this'
    * 'window'
    * 'self'
  - does NOT include the GM.* or GM_* API

* when there IS a closure:
  - the real 'window' is exposed
    by the following locally scoped variables:
    * 'unsafeWindow'
  - DOES include the entire GM.* and GM_* API

* when there IS a closure, but NOT a sandbox:
  - the real 'window' is exposed
    by the following locally scoped variables:
    * 'this'
    * 'window'
    * 'self'
    * 'globalThis'
  - the entire GM.* and GM_* API is exposed by:
    * locally scoped variables

* when there IS a closure, and IS a sandbox:
  - a proxy that restricts access to the real 'window' is exposed
    by the following locally scoped variables:
    * 'this'
    * 'window'
    * 'self'
    * 'globalThis'
  - the entire GM.* and GM_* API is exposed by:
    * locally scoped variables
    * properties of the proxy object
      - ex: 'window.GM.*' and 'window.GM_*'

---------
comments:
---------

* aside from using '@grant none' to conditionally disable a sandbox,
  the '@grant' metadata property is otherwise ignored

--------------------------------------------------------------------------------