import '@polkadot-api/substrate-client';
import { Enum } from 'polkadot-api';
import { Subject, merge, firstValueFrom } from 'rxjs';
import { createChain } from './chain.js';
import { logger } from './logger.js';
import { runPrequeries } from './prequeries.js';
import { createServer } from './serve.js';
import { createTxPool } from './txPool.js';
import { pushUmp } from './xcm.js';
import { executor } from './executor/executor.js';

const log = logger.child({ module: "forklift" });
const defaultOptions = {
  buildBlockMode: Enum("timer", 100),
  finalizeMode: Enum("timer", 2e3),
  rpcOverrides: {},
  executor
};
function forklift(source, opts) {
  let options = { ...defaultOptions, ...removeUndefinedProperties(opts) };
  const chain = createChain(source, options.executor);
  const txPool = createTxPool(chain, opts?.mockSignatureHost);
  runPrequeries(chain);
  const dmpSubject = new Subject();
  let dmpMsgQueue = [];
  const dmpSub = dmpSubject.subscribe((messages) => {
    dmpMsgQueue = [...dmpMsgQueue, ...messages];
  });
  const umpSubject = new Subject();
  let umpMsgQueues = {};
  const umpSub = umpSubject.subscribe((msg) => {
    umpMsgQueues = {
      ...umpMsgQueues,
      [msg.paraId]: [...umpMsgQueues[msg.paraId] ?? [], ...msg.messages]
    };
  });
  const hrmpSubject = new Subject();
  let hrmpMsgQueues = {};
  const hrmpSub = hrmpSubject.subscribe(({ paraId, messages }) => {
    hrmpMsgQueues = {
      ...hrmpMsgQueues,
      [paraId]: [...hrmpMsgQueues[paraId] ?? [], ...messages]
    };
  });
  let buildBlockQueue = null;
  let blocksEnqueued = 0;
  const finalizeTimers = /* @__PURE__ */ new Set();
  const newBlock = async (opts2, automatic) => {
    if (buildBlockQueue) {
      blocksEnqueued++;
      while (buildBlockQueue) {
        await buildBlockQueue;
      }
      blocksEnqueued--;
    }
    let resolve = () => {
    };
    buildBlockQueue = new Promise(async (res) => resolve = res);
    const dmp = dmpMsgQueue;
    dmpMsgQueue = [];
    const ump = umpMsgQueues;
    umpMsgQueues = {};
    for (const paraId in ump) {
      await pushUmp(chain, Number(paraId), ump[paraId]);
    }
    const hrmp = hrmpMsgQueues;
    hrmpMsgQueues = {};
    try {
      const parent = opts2?.parent ?? await firstValueFrom(chain.best$);
      const parentBlock = chain.getBlock(parent);
      const transactions = opts2?.transactions ?? await txPool.getTxsForBlock(parentBlock);
      if (automatic && transactions.length + dmp.length + Object.keys(ump).length + Object.keys(hrmp).length === 0) {
        log.debug("skipped automatic block: no transactions ready");
        return parent;
      }
      logger.info("creating block");
      const block = await chain.newBlock({
        parent,
        transactions,
        disableOnIdle: opts2?.disableOnIdle ?? options.disableOnIdle ?? false,
        xcm: { dmp, hrmp },
        storage: opts2?.storage ?? {},
        unsafeBlockHeight: opts2?.unsafeBlockHeight,
        mockSignatureHost: opts2?.mockSignatureHost ?? options.mockSignatureHost ?? false
      });
      logger.info(`block ${block.hash} created`);
      if (block.header.number > parentBlock.header.number + 1 || opts2?.finalize) {
        finalizeTimers.forEach(clearTimeout);
        finalizeTimers.clear();
        chain.changeFinalized(block.hash);
      } else if (options.finalizeMode.type === "timer" && opts2?.finalize !== false) {
        const timer = setTimeout(() => {
          try {
            chain.changeFinalized(block.hash);
          } catch {
          }
          finalizeTimers.delete(timer);
        }, options.finalizeMode.value);
        finalizeTimers.add(timer);
      }
      return block.hash;
    } catch (ex) {
      logger.error(ex, "failed creating block");
      dmpMsgQueue = [...dmp, ...dmpMsgQueue];
      for (const senderId in hrmp) {
        hrmpMsgQueues[senderId] = [
          ...hrmp[senderId] ?? [],
          ...hrmpMsgQueues[senderId] ?? []
        ];
      }
      throw ex;
    } finally {
      buildBlockQueue = null;
      resolve();
    }
  };
  let txBlockPending = false;
  const txPoolSub = merge(
    txPool.txAdded$,
    dmpSubject,
    umpSubject,
    hrmpSubject
  ).subscribe(() => {
    if (options.buildBlockMode.type === "manual") return;
    if (txBlockPending || blocksEnqueued) {
      return;
    }
    const delay = options.buildBlockMode.value;
    if (delay === 0) {
      return newBlock(void 0, true);
    }
    txBlockPending = true;
    setTimeout(() => {
      txBlockPending = false;
      if (!blocksEnqueued) newBlock(void 0, true);
    }, delay);
  });
  const xcm = {
    pushDmp: (messages) => dmpSubject.next(messages),
    pushUmp: (paraId, messages) => umpSubject.next({ paraId, messages }),
    pushHrmp: (paraId, messages) => hrmpSubject.next({ paraId, messages })
  };
  const changeOptions = (opts2) => {
    options = { ...options, ...removeUndefinedProperties(opts2) };
    serve.setRpcOverrides(options.rpcOverrides);
  };
  const serve = createServer(
    {
      source,
      chain,
      txPool,
      newBlock,
      xcm,
      getOptions: () => options,
      changeOptions
    },
    options.rpcOverrides
  );
  return {
    serve,
    newBlock: (opts2) => newBlock(opts2),
    changeFinalized: async (hash) => {
      finalizeTimers.forEach(clearTimeout);
      finalizeTimers.clear();
      return chain.changeFinalized(hash);
    },
    setStorage: async (hash, changes) => chain.setStorage(hash, changes),
    getStorageDiff: (hash, baseHash) => chain.getStorageDiff(hash, baseHash),
    changeOptions(opts2) {
      options = { ...options, ...removeUndefinedProperties(opts2) };
      serve.setRpcOverrides(options.rpcOverrides);
    },
    destroy() {
      dmpSub.unsubscribe();
      umpSub.unsubscribe();
      hrmpSub.unsubscribe();
      txPoolSub.unsubscribe();
      txPool.destroy();
      source.destroy();
    }
  };
}
const removeUndefinedProperties = (value) => value && Object.fromEntries(
  Object.entries(value).filter(([, value2]) => value2 !== void 0)
);

export { forklift };
//# sourceMappingURL=forklift.js.map
