# src/components.js

    import React, { useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue, Fragment, Suspense, lazy, createContext, } from 'react';
    import * as memoryCache from 'memory-cache';
    // import {cache} from 'memory-cache';
    // import cache from 'memory-cache';
    import { default as ReactDOMElements, } from 'react-dom-factories';
    import { getAdvancedBinding, fetchJSON, } from './utils';
    import createReactClass from 'create-react-class';
    import { getReactElementFromJSONX, } from './main';
    const cache = new memoryCache.Cache();
    // if (typeof window === 'undefined') {
    //   var window = window || global.window || {};
    // }
    /**
     * @memberOf components
     */
    export let advancedBinding = getAdvancedBinding();
    // require;
    /**
     * object of all react components available for JSONX
     * @memberOf components
     */
    export let componentMap = Object.assign({ Fragment, Suspense, }, ReactDOMElements, (typeof window ==='object') ? window.__jsonx_custom_elements : {});

    /**
     * getBoundedComponents returns reactComponents with certain elements that have this bounded to select components in the boundedComponents list
     * @memberOf components
     * @param {Object} options - options for getBoundedComponents
     * @param {Object} options.reactComponents - all react components available for JSONX
     * @param {string[]} boundedComponents - list of components to bind JSONX this context (usually helpful for navigation and redux-router)
     * @returns {Object} reactComponents object of all react components available for JSONX
     */
    export function getBoundedComponents(options = {}) {
      const { reactComponents, boundedComponents=[], } = options;
      if (advancedBinding || options.advancedBinding) {
        return Object.assign({}, reactComponents, boundedComponents.reduce((result, componentName) => {
          result[ componentName ] = reactComponents[ componentName ].bind(this);
          return result;
        }, {}));
        // reactComponents.ResponsiveLink = ResponsiveLink.bind(this);
      } else return reactComponents;
    }

    /**
     * returns a react component from a component library
     * @memberOf components
     * @param {Object} options - options for getComponentFromLibrary
     * @param {Object} [options.componentLibraries={}] - react component library like bootstrap
     * @param {Object} [options.jsonx={}] - any valid JSONX JSON
     * @returns {function|undefined} react component from react library like bootstrap, material design or bulma
     */
    export function getComponentFromLibrary(options = {}) {
      const { componentLibraries = {}, jsonx = {}, } = options;
      const libComponent = Object.keys(componentLibraries)
        .map(libraryName => {
          const cleanLibraryName = jsonx.component.replace(`${libraryName}.`, '');
          const libraryNameArray = cleanLibraryName.split('.');
          if (libraryNameArray.length === 2
            && componentLibraries[ libraryName ]
            && componentLibraries[ libraryName ][ libraryNameArray[ 0 ] ]
            && typeof componentLibraries[ libraryName ][ libraryNameArray[ 0 ] ][ libraryNameArray[ 1 ] ] !== 'undefined') {
            return componentLibraries[ libraryName ][ libraryNameArray[ 0 ] ][ libraryNameArray[ 1 ] ];
          } else if (typeof componentLibraries[ libraryName ][ cleanLibraryName ] !== 'undefined') {
            return componentLibraries[ libraryName ][ cleanLibraryName ];
          }
        })
        .filter(val => val)[ 0 ];
      return libComponent;
    }

    /**
     * returns a react element from jsonx.component
     * @memberOf components
     * @example
     * // returns react elements
     * getComponentFromMap({jsonx:{component:'div'}})=>div
     * getComponentFromMap({jsonx:{component:'MyModal'},reactComponents:{MyModal:MyModal extends React.Component}})=>MyModal
     * getComponentFromMap({jsonx:{component:'reactBootstap.nav'},componentLibraries:{reactBootstrap,}})=>reactBootstap.nav
     * @param {Object} options - options for getComponentFromMap
     * @param {object} [options.jsonx={}] - any valid JSONX JSON object
     * @param {Object} [options.reactComponents={}] - react components to render
     * @param {Object} [options.componentLibraries={}] - react components to render from another component library like bootstrap or bulma
     * @param {function} [options.logError=console.error] - error logging function
     * @param {boolean} [options.debug=false] - use debug messages
     * @returns {string|function|class} valid react element
     */
    export function getComponentFromMap(options = {}) {
      // eslint-disable-next-line
      const { jsonx = {}, reactComponents = {}, componentLibraries = {}, logError = console.error, debug } = options;

      try {
        if (typeof jsonx.component !== 'string' && typeof jsonx.component === 'function') {
          return jsonx.component;
        } else if (ReactDOMElements[jsonx.component]) {
          return jsonx.component;
        } else if (reactComponents[ jsonx.component ]) {
          return reactComponents[jsonx.component];
        } else if (typeof jsonx.component ==='string' && jsonx.component.indexOf('.') > 0 && getComponentFromLibrary({ jsonx, componentLibraries, })) {
          return getComponentFromLibrary({ jsonx, componentLibraries, });
        } else {
          throw new ReferenceError(`Invalid React Component (${jsonx.component})`);
        }
      } catch (e) {
        if(debug) logError(e, (e.stack) ? e.stack : 'no stack');
        throw e;
      }
    }

    /**
     * Returns a new function from an options object
     * @memberOf components
     * @param {Object} options
     * @param {String} [options.body=''] - Function string body
     * @param {String[]} [options.args=[]] - Function arguments
     * @returns {Function}
     */
    export function getFunctionFromEval(options = {}) {
      if (typeof options === 'function') return options;
      const { body = '', args = [], name, } = options;
      const argus = [].concat(args);
      argus.push(body);
      const evalFunction = Function.prototype.constructor.apply({ name, }, argus);
      if (name) {
        Object.defineProperty(evalFunction, 'name', { value: name, });
      }
      return evalFunction;
    }

    /**
     * Returns a new React Component
     * @memberOf components
     * @param {Boolean} [options.returnFactory=true] - returns a React component if true otherwise returns Component Class
     * @param {Object} [options.resources={}] - asyncprops for component
     * @param {String} [options.name ] - Component name
     * @param {Function} [options.lazy ] - function that resolves {reactComponent,options} to lazy load component for code splitting
     * @param {Boolean} [options.use_getState=true] - define getState prop
     * @param {Boolean} [options.bindContext=true] - bind class this reference to render function components
     * @param {Boolean} [options.passprops ] - pass props to rendered component
     * @param {Boolean} [options.passstate] - pass state as props to rendered component
     * @param {Object} [reactComponent={}] - an object of functions used for create-react-class
     * @param {Object} reactComponent.render.body - Valid JSONX JSON
     * @param {String} reactComponent.getDefaultProps.body - return an object for the default props
     * @param {String} reactComponent.getInitialState.body - return an object for the default state
     * @returns {Function}
     * @see {@link https://reactjs.org/docs/react-without-es6.html}
     */
    export function getReactClassComponent(reactComponent = {}, options = {}) {
      // const util = require('util');
      // console.log(util.inspect({ reactComponent },{depth:20}));
      if (options.lazy) {
        return lazy(() => options.lazy(reactComponent, Object.assign({}, options, { lazy: false, })).then((lazyComponent) => {
          return {
            default: getReactClassComponent(...lazyComponent),
          };
        }));
      }
      const context = this || {};
      const { returnFactory = true, resources = {}, use_getState=true, bindContext=true, disableRenderIndexKey = true, } = options;
      const rjc = Object.assign({
        getDefaultProps: {
          body:'return {};',
        },
        getInitialState: {
          body:'return {};',
        },
      }, reactComponent);
      const rjcKeys = Object.keys(rjc);
      if (rjcKeys.includes('render') === false) {
        throw new ReferenceError('React components require a render method');
      }
      const classOptions = rjcKeys.reduce((result, val) => {
        if (typeof rjc[ val ] === 'function') rjc[ val ] = { body: rjc[ val ], };
        const args = rjc[ val ].arguments;
        const body = rjc[ val ].body;
        if (!body) {
          console.warn({ rjc, });
          throw new SyntaxError(`Function(${val}) requires a function body`);
        }
        if (args && !Array.isArray(args) && (args.length &&(args.length && args.filter(arg=>typeof arg==='string').length)) ) {
          throw new TypeError(`Function(${val}) arguments must be an array or variable names`);
        }
        if (val === 'render') {
          result[ val ] = function () {
            if (options.passprops && this.props) body.props = Object.assign({}, body.props, this.props);
            if (options.passstate && this.state) body.props = Object.assign({}, body.props, this.state);
            return getReactElementFromJSONX.call(Object.assign(
              {},
              context,
              bindContext ? this : {},
              { disableRenderIndexKey, },
              {
                props: use_getState
                  ? Object.assign({}, this.props, { getState: () => this.state, })
                  : this.props,
              }
            ), body, resources);
          };
        } else {
          result[ val ] = typeof body === 'function'
            ? body
            : getFunctionFromEval({
              body,
              args,
            });
        }

        return result;
      }, {});
      const reactComponentClass = createReactClass(classOptions);
      if (options.name) {
        Object.defineProperty(
          reactComponentClass,
          'name',
          {
            value: options.name,
          }
        );
      }
      const reactClass = returnFactory
        ? React.createFactory(reactComponentClass)
        : reactComponentClass;
      return reactClass;
    }

    export function DynamicComponent(props={}) {
      const { useCache = true, cacheTimeout = 60 * 60 * 5, loadingJSONX= { component:'div', children:'...Loading', },
      loadingErrorJSONX= { component:'div', children:[{component:'span',children:'Error: '},{ component:'span',  resourceprops:{_children:['error','message']}, }], }, cacheTimeoutFunction = () => { }, jsonx, transformFunction = data => data, fetchURL, fetchOptions, fetchFunction, } = props;
      const context = this || {};
      const [ state, setState ] = useState({ hasLoaded: false, hasError: false, resources: {}, error:undefined, });
      const transformer = useMemo(()=>getFunctionFromEval(transformFunction), [ transformFunction ]);
      const timeoutFunction = useMemo(()=>getFunctionFromEval(cacheTimeoutFunction), [ cacheTimeoutFunction ]);
      const renderJSONX = useMemo(()=>getReactElementFromJSONX.bind(context), [ context ]);
      const loadingComponent = useMemo(()=>renderJSONX(loadingJSONX), [ loadingJSONX ]);
      const loadingError = useMemo(()=>renderJSONX(loadingErrorJSONX,{error:state.error}), [ loadingErrorJSONX, state.error ]);

      useEffect(() => {
        async function getData() {
          try {
            let transformedData;
            if (useCache && cache.get(fetchURL)) {
              transformedData = cache.get(fetchURL);
            } else {
              let fetchedData;
              if (fetchFunction) {
                fetchedData = await fetchFunction(fetchURL, fetchOptions);
              } else fetchedData = await fetchJSON(fetchURL, fetchOptions);
              transformedData = await transformer(fetchedData);
              if (useCache) cache.put(fetchURL, transformedData, cacheTimeout,timeoutFunction);
            }
            setState(prevState=>Object.assign({},prevState,{ hasLoaded: true, hasError: false, resources: { DynamicComponentData: transformedData, }, }));
          } catch (e) {
            if(context.debug) console.warn(e);
            setState({ hasError: true, error:e, });
          }
        }
        if(fetchURL) getData();
      }, [ fetchURL, fetchOptions ]);
      if (!fetchURL) return null;
      else if (state.hasError) {
        return loadingError;
      } else if (state.hasLoaded === false) {
        return loadingComponent;
      } else return renderJSONX(jsonx, state.resources);
    }

    /**
     * Returns new React Function Component
     * @memberOf components
     * @todo set 'functionprops' to set arguments for function
     * @param {*} reactComponent - Valid JSONX to render
     * @param {String} functionBody - String of function component body
     * @param {String} options.name - Function Component name
     * @returns {Function}
     * @see {@link https://reactjs.org/docs/hooks-intro.html}
     * @example
      const jsonxRender = {
       component:'div',
       passprops:'true',
       children:[
         {
          component:'input',
          thisprops:{
              value:['count'],
            },
         },
          {
            component:'button',
           __dangerouslyBindEvalProps:{
            onClick:function(count,setCount){
              setCount(count+1);
              console.log('this is inline',{count,setCount});
            },
            // onClick:`(function(count,setCount){
            //   setCount(count+1)
            //   console.log('this is inline',{count,setCount});
            // })`,
            children:'Click me'
          }
       ]
      };
      const functionBody = 'const [count, setCount] = useState(0); const functionprops = {count,setCount};'
      const options = { name: IntroHook}
      const MyCustomFunctionComponent = jsonx._jsonxComponents.getReactFunctionComponent({jsonxRender, functionBody, options});
       */
    export function getReactFunctionComponent(reactComponent = {}, functionBody = '', options = {}) {
      if (options.lazy) {
        return lazy(() => options.lazy(reactComponent, functionBody, Object.assign({}, options, { lazy: false, })).then((lazyComponent) => {
          return {
            default: getReactFunctionComponent(...lazyComponent),
          };
        }));
      }
      if (typeof options === 'undefined' || typeof options.bind === 'undefined') options.bind = true;
      const { resources = {}, args=[], } = options;

      const props = reactComponent.props;
      const functionArgs = [ React, useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue, getReactElementFromJSONX, reactComponent, resources, props, ];
      if (typeof functionBody === 'function') functionBody = functionBody.toString();
      const functionComponent = Function('React', 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect', 'useDebugValue', 'getReactElementFromJSONX', 'reactComponent', 'resources', 'props', `
        const self = this;
        return function ${options.name || 'Anonymous'}(props){
          ${functionBody}
          if(typeof exposeprops==='undefined' || exposeprops){
            reactComponent.props = Object.assign({},props,typeof exposeprops==='undefined'?{}:exposeprops);
            // reactComponent.__functionargs = Object.keys(exposeprops);
          } else{
            reactComponent.props =  props;
          }
          if(!props.children) delete props.children;
          const context = ${options.bind?'Object.assign(self,this)':'this'};
          return getReactElementFromJSONX.call(context, reactComponent);
        }
      `);
      if (options.name) {
        Object.defineProperty(
          functionComponent,
          'name',
          {
            value: options.name,
          }
        );
      }
      return (options.bind) ? functionComponent.call(this, ...functionArgs) : functionComponent(...functionArgs);
    }
    /**
     * @memberOf components
     */
    export function getReactContext(options = {}) {
      return createContext(options.value);
    }