
// imports
import dynamic from 'next/dynamic';
import { ErrorBoundary } from 'react-error-boundary';
import React, { useMemo } from 'react';

// locals
import Image from '../components/Image';
import Input from '../components/Input';
import Button from '../components/Button';
import Loading from '../components/Loading';
import NodeBase from '../components/Node/Base';
import ApiSignal from './Api';
import NodeChoose from '../components/Node/Choose';
import ButtonIcon from '../components/Button/Icon';
import Signal, { effect } from '../signal';
import { current as TeamSignal } from './Teams';

// components
import NodeComponents from '../components/Node/Components/index';

// dynamic editor
const Editor = dynamic(() => import('@monaco-editor/react'), {
  loading: () => <div className="" />,
});

// refreshing 
export let refreshing = false;

// uuid
import shortid from 'short-unique-id';
const uid = new shortid({
  length     : 10,
  dictionary : 'hex',
});

// create signal
const NodesSignal = new Signal.State({});
export const ComponentsSignal = new Signal.State({});

// refresh
export const refresh = async () => {
  // refreshing
  if (refreshing) return;

  // refreshing
  refreshing = true;

  // try
  try {
    // nodes
    const { nodes } = await ApiSignal.get().Node.list({
      team  : TeamSignal.get()?.id,
      limit : 1000,
    });

    // set nodes
    NodesSignal.set(nodes.reduce((accum, node) => {
      // set to accum
      accum[node.id] = node;

      // return accum
      return accum;
    }, {}));

    // set components signal
    ComponentsSignal.set([
      ...(Object.keys(NodesSignal.get()) || []),
      'choose',
    ].reduce((accum, key) => {
      // if choose
      if (key === 'choose') {
        // set accum
        accum[key] = (props = {}) => {
          // choose node
          return (
            <NodeChoose
              {...props}
              type={key}
              item={props.data}
              node={NodesSignal.get()[key]}
              nodes={NodesSignal.get()}
              onChange={props.data.onChange}
            />
          );
        };
      } else {
        // set accum
        accum[key] = (props = {}) => {
          // item
          const inputs = Object.values([
            ...(NodesSignal.get()[key].inputs || []),
            ...(props.data?.inputs || []),
          ].reduce((accum, input) => {
            // set accum
            accum[input.id] = input;

            // return accum
            return accum;
          }, {}));
          const outputs = Object.values([
            ...(NodesSignal.get()[key].outputs || []),
            ...(props.data?.outputs || []),
          ].reduce((accum, output) => {
            // set accum
            accum[output.id] = output;

            // return accum
            return accum;
          }, {}));

          // string
          const ChildComponent = useMemo(() => {
            // component
            const actualComponent = `(props) => {
              const { inputs, outputs, onInputs, onOutputs, variant } = props;
              const { uid, Editor, Image, Input, Button, ButtonIcon, Loading, item, node, spec, output, input, onSpec } = props;
              const { ${Object.keys(NodeComponents).join(', ')} } = props.Components;
              
              ${NodesSignal.get()[key].display}
            }`;

            // try/catch
            try {
              // create react element from string
              return new Function('React', 'return ' + actualComponent)(React);
            } catch (e) {
              // log
              console.log(e);

              // return error
              return () => <div>Error</div>;
            }
          }, [NodesSignal.get()[key].display]);

          // element
          const NodeElement = useMemo(() => {
            // return new component
            return (renderProps = {}) => {

              // return jsx
              return (
                <ErrorBoundary fallback={<div>error</div>}>
                  <ChildComponent
                    item={renderProps.data}
                    node={NodesSignal.get()[key]}
                    spec={renderProps.data?.spec}
                    input={renderProps.input || {}}
                    inputs={renderProps.inputs}
                    output={renderProps.output || {}}
                    outputs={renderProps.outputs}
                    variant={renderProps.variant}
                    shouldLog={renderProps.shouldLog}

                    onSpec={(key, value) => {
                      // message
                      if (renderProps.variant === 'message') return;

                      // check diff
                      if (JSON.stringify(renderProps.data?.spec?.[key] || '') === JSON.stringify(value)) return;
  
                      // on change
                      renderProps.data.onChange('spec', {
                        ...(renderProps.data?.spec || {}),
                        [key] : value,
                      });
                    }}
                    onInputs={(newInputs) => {
                      // message
                      if (renderProps.variant === 'message') return;
  
                      // on change
                      renderProps.data.onChange('inputs', [...newInputs]);
                    }}
                    onOutputs={(newOutputs) => {
                      // message
                      if (renderProps.variant === 'message') return;
  
                      // on change
                      renderProps.data.onChange('outputs', [...newOutputs]);
                    }}

                    Components={NodeComponents}
                    
                    // other elements
                    uid={uid}
                    Input={Input}
                    Image={Image}
                    Editor={Editor}
                    Button={Button}
                    Loading={Loading}
                    ButtonIcon={ButtonIcon}
                  />
                </ErrorBoundary>
              );
            };
          }, [props.id]);

          // choose node
          return (
            <NodeBase
              {...props}
              type={key}
              item={props.data}
              node={NodesSignal.get()[key]}
              onChange={props.data.onChange}

              inputs={inputs}
              outputs={outputs}

              Element={NodeElement}
            />
          );
        };
      }
      
      // return accum
      return accum;
    }, {}));
  } catch (e) {
    // log
    console.log(e);
  } finally {
    // refreshing
    refreshing = false;
  }
};

// create effect
effect(() => {
  // check user id
  if (!ApiSignal.get()) return;
  if (!TeamSignal.get()?.id) return;
  if (typeof window === 'undefined') return;
  
  // refresh
  refresh();
});

// export default
export default NodesSignal;