import toposort from 'toposort';

const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;

export class UtilRegistry {

  _registeredUtilClasses = new Array(); //{utilClass}
  _activeUtilInstancesWrapped = new Array(); //{utilClass, scope, element, instance}
  _appInstance;

  /**
   * function registerUtil
   *
   * utils need to have at least these properties:
   *   name: string | utils name, e.g. 'example'
   *   selector: string | utils selector, e.g. '[uw-example]'
   *   setup: Function | utils setup function, see below
   * 
   * optional util properties:
   *   start: Function | utils start function, see below
   *
   * setup function must return instance object with at least these properties:
   *   name: string | utils name
   *   element: HTMLElement | element the util is applied to
   *   destroy: Function | function to destroy the util and remove any listeners
   *
   * (optional) start function for registering event listeners
   *
   * @param util Object Utility that should be added to the registry
   */
  register(util) {
    if (DEBUG_MODE > 2) {
      console.log('registering util "' + util.name + '"');
      console.log({ util });
    }
    this._registeredUtilClasses.push(util);
  }

  deregister(name, destroy) {
    const utilIndex = this._findUtilIndex(name);

    if (utilIndex >= 0) {
      if (destroy === true) {
        this._destroyUtilInstances(name);
      }

      this._registeredUtilClasses.splice(utilIndex, 1);
    }
  }

  setApp(appInstance) {
    this._appInstance = appInstance;
  }

  initAll(scope = document.body) {
    let startedInstances = new Array();
    const setupInstances = this._registeredUtilClasses.map((util) => this.setup(util, scope)).flat();

    const orderedInstances = setupInstances.filter(_isStartOrdered);

    if (DEBUG_MODE > 3) {
      console.log({ setupInstances, orderedInstances });
    }

    const startDependencies = new Array();
    for (const utilInstance of orderedInstances) {
      for (const otherInstance of setupInstances) {
        const startOrder = _startOrder(utilInstance, otherInstance);
        if (typeof startOrder !== 'undefined')
          startDependencies.push(startOrder);
      }
    }

    if (DEBUG_MODE > 2) {
      console.log('starting instances', { setupInstances, startDependencies, order: toposort.array(setupInstances, startDependencies) });
    }

    toposort.array(setupInstances, startDependencies).forEach((utilInstance) => {
      if (utilInstance) {
        if (DEBUG_MODE > 2) {
          console.log('starting utilInstance', { util: utilInstance.util.name, utilInstance });
        }

        const instance = utilInstance.instance;
        if (instance && typeof instance.start === 'function') {
          instance.start.bind(instance)();
          startedInstances.push(instance);
        }
      }
    });

    if (DEBUG_MODE > 1) {
      console.info('initialized js util instances:');
      console.table(setupInstances);
    }

    return startedInstances;
  }

  destroyAll(scope = document.body) {
    let utilsInScope = this._getUtilInstancesWithinScope(scope);

    utilsInScope.forEach((util) => {
      if(DEBUG_MODE > 2) {
        console.log('Destroying Util: ', {util});
      }
      util.destroy();
      this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter(utilWrapped => {
        return utilWrapped.element === util._element;
      });
    });
  }

  setup(util, scope = document.body) {
    if (DEBUG_MODE > 2) {
      console.log('setting up util', { util });
    }

    let instances = new Array();

    if (util) {
      const elements = this._findUtilElements(util, scope);

      elements.forEach((element) => {
        let utilInstance = null;

        try {
          utilInstance = new util(element, this._appInstance);
        } catch(err) {
          if (DEBUG_MODE > 0) {
            console.error('Error while trying to initialize a utility!', { util , element, err });
            console.error(err.stack);
          }
          utilInstance = null;
        }

        if (utilInstance) {
          if (DEBUG_MODE > 2) {
            console.info('Got utility instance for utility "' + util.name + '"', { utilInstance });
          }

          instances.push({ util: util, scope: scope, element: element, instance: utilInstance });
        }
      });
    }

    this._activeUtilInstancesWrapped.push(...instances);
    return instances;
  }

  find(name) {
    return this._registeredUtilClasses.find((util) => util.name === name);
  }

  _findUtilElements(util, scope) {
    if (scope && scope.matches(util.selector)) {
      return [scope];
    }
    return Array.from(scope.querySelectorAll(util.selector));
  }

  _findUtilIndex(name) {
    return this._registeredUtilClasses.findIndex((util) => util.name === name);
  }

  _getUtilInstancesWithinScope(scope) {
    let utilInstances = [];

    for (let activeUtilInstance of this._activeUtilInstancesWrapped) {
      let util = activeUtilInstance.util;
      if(this._findUtilElements(util, scope).length > 0) {
        utilInstances.push(activeUtilInstance.instance);
      }
    }
    return utilInstances;
  }

  _destroyUtilInstances(name) {
    this._activeUtilInstancesWrapped
      .map((util, index) => ({
        util: util,
        index: index,
      }))
      .filter((activeUtil) => activeUtil.util.util.name === name)
      .forEach((activeUtil) => {
        // destroy util instance
        activeUtil.util.instance.destroy();
        this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.splice(activeUtil.index, 1);
      });
  }
}

function _startOrder(utilInstance, otherInstance) {
  if (utilInstance.element !== otherInstance.element && !(utilInstance.element.contains(otherInstance.element) || otherInstance.element.contains(utilInstance.element)))
    return undefined;
  
  if (utilInstance === otherInstance)
    return undefined;

  if (!_isStartOrdered(utilInstance) || !otherInstance.instance || !otherInstance.util)
    return undefined;

  function orderParam(name) {
    if (typeof utilInstance.instance[name] === 'function')
      return !!utilInstance.instance[name](otherInstance.instance);
    if (typeof utilInstance.util[name] === 'function')
      return !!utilInstance.util[name](otherInstance.instance);
    else if (Array.isArray(utilInstance.instance[name]))
      return utilInstance.instance[name].some(constr => otherInstance.util === constr);
    else if (Array.isArray(utilInstance.util[name]))
      return utilInstance.util[name].some(constr => otherInstance.util === constr);

    return false;
  }

  const after = orderParam('startAfter');
  const before = orderParam('startBefore');

  if (DEBUG_MODE > 3) {
    console.log('compared instances for ordering', { utilInstance, otherInstance }, { after, before });
  }

  if (after && before) {
    console.error({ utilInstance, otherInstance });
    throw new Error(`Incompatible start ordering: ${utilInstance.instance.constructor.name} and ${otherInstance.instance.constructor.name}`);
  } else if (after)
    return [otherInstance, utilInstance];
  else if (before)
    return [utilInstance, otherInstance];

  return undefined;
}

function _isStartOrdered(utilInstance) {
  if (!utilInstance || !utilInstance.instance || !utilInstance.util)
    return false;
  
  function isOrderParam(name) {
    return typeof utilInstance.instance[name] === 'function' ||
      typeof utilInstance.util[name] === 'function' ||
      Array.isArray(utilInstance.instance[name]) ||
      Array.isArray(utilInstance.util[name]);
  }

  return isOrderParam('startBefore') || isOrderParam('startAfter');
}
