// src/index.ts
import { is_object_literal, is_object } from "mixme";
import * as toposort from "toposort";

// src/error.ts
var PlugableError = class _PlugableError extends Error {
  code;
  constructor(code, message, ...contexts) {
    if (Array.isArray(message)) {
      message = message.filter(function(line) {
        return !!line;
      }).join(" ");
    }
    message = `${code}: ${message}`;
    super(message);
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, _PlugableError);
    }
    this.code = code;
    for (let i = 0; i < contexts.length; i++) {
      const context = contexts[i];
      for (const key in context) {
        if (key === "code") {
          continue;
        }
        const value = context[key];
        if (value === void 0) {
          continue;
        }
        this[key] = Buffer.isBuffer(value) ? value.toString() : value === null ? value : JSON.parse(JSON.stringify(value));
      }
    }
  }
};
var error_default = function(...args) {
  return new PlugableError(...args);
};

// src/index.ts
var plugandplay = function({
  args = [],
  chain,
  parent,
  plugins = []
} = {}) {
  const store = [];
  const api = {
    // Register new plugins
    register: function(plugin) {
      if (typeof plugin !== "function" && !is_object_literal(plugin)) {
        throw error_default("PLUGINS_REGISTER_INVALID_ARGUMENT", [
          "a plugin must be an object literal or a function returning an object literal",
          "with keys such as `name`, `required` and `hooks`,",
          `got ${JSON.stringify(plugin)} instead.`
        ]);
      }
      plugin = typeof plugin === "function" ? plugin(...args) : plugin;
      const hooksNormalized = {};
      for (const name in plugin.hooks) {
        if (!plugin.hooks[name]) continue;
        hooksNormalized[name] = normalize_hook(name, plugin.hooks[name]);
      }
      if (plugin.require && !Array.isArray(plugin.require)) {
        plugin.require = [plugin.require];
      }
      const require2 = !plugin.require ? [] : !Array.isArray(plugin.require) ? [plugin.require] : plugin.require;
      const requireNormalized = require2.map((require3) => {
        if (typeof require3 !== "string")
          throw errors.PLUGINS_REGISTER_INVALID_REQUIRE({
            name: plugin.name,
            require: require3
          });
        return require3;
      });
      const normalizedPlugin = {
        hooks: hooksNormalized,
        require: requireNormalized,
        name: plugin.name
      };
      store.push(normalizedPlugin);
      return chain ?? this;
    },
    registered: function(name) {
      for (const plugin of store) {
        if (plugin.name === name) {
          return true;
        }
      }
      if (parent != null && parent.registered(name)) {
        return true;
      }
      return false;
    },
    get: function({ name, hooks = [], sort = true }) {
      const normalizedHooks = [
        // Merge hooks provided by the user
        ...normalize_hook(name, hooks),
        // With hooks present in the store
        ...store.map(function(plugin) {
          if (!plugin.hooks[name]) return;
          for (const require2 of plugin.require) {
            if (!api.registered(require2)) {
              throw errors.REQUIRED_PLUGIN({
                plugin: plugin.name,
                require: require2
              });
            }
          }
          return plugin.hooks[name]?.map(
            (hook) => ({
              plugin: plugin.name,
              require: plugin.require,
              ...hook
            })
          );
        }).filter(function(hook) {
          return hook !== void 0;
        }).flat(1),
        ...parent ? parent.get({ name, sort: false }) : []
      ];
      if (!sort) {
        return normalizedHooks;
      }
      const index = {};
      for (const hook of normalizedHooks) {
        if (hook.plugin) index[hook.plugin] = hook;
      }
      const edges_after = normalizedHooks.map(function(hook) {
        return hook.after.reduce(function(result, after) {
          if (index[after]) {
            result.push([index[after], hook]);
          } else if (api.registered(after)) {
            throw errors.PLUGINS_HOOK_AFTER_INVALID({
              name,
              plugin: hook.plugin,
              after
            });
          }
          return result;
        }, []);
      });
      const edges_before = normalizedHooks.map(function(hook) {
        return hook.before.reduce(function(result, before) {
          if (index[before]) {
            result.push([hook, index[before]]);
          } else if (api.registered(before)) {
            throw errors.PLUGINS_HOOK_BEFORE_INVALID({
              name,
              plugin: hook.plugin,
              before
            });
          }
          return result;
        }, []);
      });
      const edges = [...edges_after, ...edges_before].flat(1);
      return toposort.array(normalizedHooks, edges);
    },
    // Call a hook against each registered plugin matching the hook name
    call: async function({ args: args2, handler, hooks = [], name }) {
      if (arguments.length !== 1) {
        throw error_default("PLUGINS_INVALID_ARGUMENTS_NUMBER", [
          "function `call` expect 1 object argument,",
          `got ${arguments.length} arguments.`
        ]);
      } else if (!is_object_literal(arguments[0])) {
        throw error_default("PLUGINS_INVALID_ARGUMENT_PROPERTIES", [
          "function `call` expect argument to be a literal object",
          "with the properties `name`, `args`, `hooks` and `handler`,",
          `got ${JSON.stringify(arguments[0])} arguments.`
        ]);
      } else if (typeof name !== "string") {
        throw error_default("PLUGINS_INVALID_ARGUMENT_NAME", [
          "function `call` requires a property `name` in its first argument,",
          `got ${JSON.stringify(arguments[0])} argument.`
        ]);
      }
      const hooksNormalized = this.get({
        hooks,
        name
      });
      handler = handler || (() => {
      });
      for (const hook of hooksNormalized) {
        switch (hook.handler.length) {
          case 0:
          case 1:
            await hook.handler(args2, () => {
            });
            break;
          case 2:
            const result = await hook.handler(args2, handler);
            if (result === null) {
              return null;
            } else {
              handler = result;
            }
            break;
          default:
            throw error_default("PLUGINS_INVALID_HOOK_HANDLER", [
              "hook handlers must have 0 to 2 arguments",
              `got ${hook.handler.length}`
            ]);
        }
      }
      return handler ? handler(args2, () => {
      }) : void 0;
    },
    // Call a hook against each registered plugin matching the hook name
    call_sync: function({ args: args2, handler, hooks = [], name }) {
      if (arguments.length !== 1) {
        throw error_default("PLUGINS_INVALID_ARGUMENTS_NUMBER", [
          "function `call` expect 1 object argument,",
          `got ${arguments.length} arguments.`
        ]);
      } else if (!is_object_literal(arguments[0])) {
        throw error_default("PLUGINS_INVALID_ARGUMENT_PROPERTIES", [
          "function `call` expect argument to be a literal object",
          "with the properties `name`, `args`, `hooks` and `handler`,",
          `got ${JSON.stringify(arguments[0])} arguments.`
        ]);
      } else if (typeof name !== "string") {
        throw error_default("PLUGINS_INVALID_ARGUMENT_NAME", [
          "function `call` requires a property `name` in its first argument,",
          `got ${JSON.stringify(arguments[0])} argument.`
        ]);
      }
      const hooksNormalized = this.get({
        hooks,
        name
      });
      handler = handler || (() => {
      });
      for (const hook of hooksNormalized) {
        switch (hook.handler.length) {
          case 0:
          case 1:
            hook.handler(args2, () => {
            });
            break;
          case 2:
            const result = hook.handler(args2, handler);
            if (result === null) {
              return null;
            } else {
              handler = result;
            }
            break;
          default:
            throw error_default("PLUGINS_INVALID_HOOK_HANDLER", [
              "hook handlers must have 0 to 2 arguments",
              `got ${hook.handler.length}`
            ]);
        }
      }
      return handler ? handler(args2, () => {
      }) : void 0;
    }
  };
  for (const plugin of plugins) {
    api.register(plugin);
  }
  return api;
};
var normalize_hook = function(name, hook) {
  const hooks = Array.isArray(hook) ? hook : [hook];
  return hooks.map(function(hook2) {
    if (typeof hook2 !== "function" && !is_object(hook2)) {
      throw error_default("PLUGINS_HOOK_INVALID_HANDLER", [
        "no hook handler function could be found,",
        "a hook must be defined as a function",
        "or as an object with an handler property,",
        `got ${JSON.stringify(hook2)} instead.`
      ]);
    }
    return {
      after: !(typeof hook2 !== "function" && hook2.after) ? [] : typeof hook2.after === "string" ? [hook2.after] : hook2.after,
      name,
      before: !(typeof hook2 !== "function" && hook2.before) ? [] : typeof hook2.before === "string" ? [hook2.before] : hook2.before,
      handler: typeof hook2 === "function" ? hook2 : hook2.handler
    };
  });
};
var errors = {
  PLUGINS_HOOK_AFTER_INVALID: function({
    name,
    plugin,
    after
  }) {
    throw error_default("PLUGINS_HOOK_AFTER_INVALID", [
      `the hook ${JSON.stringify(name)}`,
      plugin ? `in plugin ${JSON.stringify(plugin)}` : "",
      "references an after dependency",
      `in plugin ${JSON.stringify(after)} which does not exists.`
    ]);
  },
  PLUGINS_HOOK_BEFORE_INVALID: function({
    name,
    plugin,
    before
  }) {
    throw error_default("PLUGINS_HOOK_BEFORE_INVALID", [
      `the hook ${JSON.stringify(name)}`,
      plugin ? `in plugin ${JSON.stringify(plugin)}` : "",
      "references a before dependency",
      `in plugin ${JSON.stringify(before)} which does not exists.`
    ]);
  },
  REQUIRED_PLUGIN: function({
    plugin,
    require: require2
  }) {
    throw error_default("REQUIRED_PLUGIN", [
      `the plugin ${JSON.stringify(plugin)}`,
      "requires a plugin",
      `named ${JSON.stringify(require2)} which is not unregistered.`
    ]);
  },
  PLUGINS_REGISTER_INVALID_REQUIRE: function({
    name,
    require: require2
  }) {
    throw error_default("PLUGINS_REGISTER_INVALID_REQUIRE", [
      "the `require` property",
      name ? `in plugin ${JSON.stringify(name)}` : "",
      "must be a string or an array,",
      `got ${JSON.stringify(require2)}.`
    ]);
  }
};
export {
  PlugableError,
  plugandplay
};
//# sourceMappingURL=index.js.map