(() => {
  const LIQUIDITY_EVENT = 0x4c514533; // LQE3
  const JETTON_NOTIFICATION = 0x7362d09c;

  // Only ordinary cells in the standard TON BoC format are needed for these logs.
  function readCells(body, { maxLength = 8192, maxCells = 64 } = {}) {
    if (typeof body !== 'string' || body.length > maxLength) throw new Error('Invalid TON message body');
    const bytes = atob(body);
    if (bytes.slice(0, 4) !== '\xb5\xee\x9c\x72') throw new Error('Invalid TON BoC header');
    let cursor = 4;
    let end = bytes.length;
    function read(length) {
      if (cursor + length > end) throw new Error('Truncated TON BoC');
      let value = 0;
      for (let i = 0; i < length; i++) value = value * 256 + bytes.charCodeAt(cursor++);
      return value;
    }
    const flags = read(1);
    const size = flags & 7;
    const offsetSize = read(1);
    if (size < 1 || size > 4 || offsetSize < 1 || offsetSize > 4 || flags & 0x38) {
      throw new Error('Unsupported TON BoC header');
    }
    const count = read(size);
    const rootCount = read(size);
    const absent = read(size);
    const cellBytes = read(offsetSize);
    const root = read(size);
    if (count < 1 || count > maxCells || rootCount !== 1 || absent !== 0 || root >= count) {
      throw new Error('Unsupported TON BoC graph');
    }
    if (flags & 0x80) cursor += count * offsetSize;
    if (cursor + cellBytes + ((flags & 0x40) ? 4 : 0) !== end) throw new Error('Invalid TON BoC size');
    end = cursor + cellBytes;
    const cells = [];
    for (let i = 0; i < count; i++) {
      const start = cursor;
      const refs = read(1);
      const descriptor = read(1);
      if (refs > 4) throw new Error('Unsupported TON cell descriptor');
      let bits = '';
      for (let j = 0; j < Math.ceil(descriptor / 2); j++) bits += read(1).toString(2).padStart(8, '0');
      if (descriptor % 2) {
        const padding = bits.lastIndexOf('1');
        if (padding < bits.length - 7) throw new Error('Invalid TON cell padding');
        bits = bits.slice(0, padding);
      }
      const data = bytes.slice(start, cursor);
      const references = Array.from({ length: refs }, () => read(size));
      if (references.some(index => index <= i || index >= count)) throw new Error('Invalid TON cell reference');
      cells.push({ bits, data, references });
    }
    if (cursor !== end) throw new Error('Unexpected TON BoC data');
    return { cells, root };
  }

  function reader({ bits }) {
    let position = 0;
    function uint(size) {
      if (position + size > bits.length) throw new Error('Truncated TON cell');
      const value = bits.slice(position, position + size);
      position += size;
      return value ? BigInt('0b' + value) : 0n;
    }
    function readAddress(tag) {
      if (tag !== 2n || uint(1) !== 0n) throw new Error('Unsupported TON address');
      const workchain = Number(uint(8)) << 24 >> 24;
      return workchain + ':' + uint(256).toString(16).padStart(64, '0');
    }
    return {
      uint,
      int(size) {
        const value = uint(size);
        return BigInt.asIntN(size, value);
      },
      coins: () => uint(Number(uint(4)) * 8),
      address: () => readAddress(uint(2)),
      maybeAddress() {
        const tag = uint(2);
        return tag === 0n ? null : readAddress(tag);
      },
      end() {
        if (position !== bits.length) throw new Error('Unexpected TON cell fields');
      },
    };
  }

  function liquidityEvent(body) {
    const { cells, root } = readCells(body);
    const { references } = cells[root];
    if (cells.length !== 2 || references.length !== 1 || cells[references[0]].references.length) {
      throw new Error('Invalid DLMM liquidity event cells');
    }
    const log = reader(cells[root]);
    if (log.uint(32) !== BigInt(LIQUIDITY_EVENT)) throw new Error('Invalid DLMM liquidity opcode');
    const kind = Number(log.uint(8));
    if (![1, 2, 3].includes(kind)) throw new Error('Unsupported DLMM liquidity action');
    const queryId = log.uint(64).toString();
    const owner = log.address();
    log.uint(32); // Price bin.
    log.uint(256); // Shares.
    log.uint(32); // Active bin.
    log.end();
    const amounts = reader(cells[references[0]]);
    const t3 = amounts.coins();
    const asset = amounts.coins();
    amounts.end();
    return { kind, queryId, owner, t3, asset };
  }

  function depositPayer(message, queryId) {
    if (message?.op !== JETTON_NOTIFICATION) return null;
    const { cells, root } = readCells(message.body);
    const notification = reader(cells[root]);
    if (notification.uint(32) !== BigInt(JETTON_NOTIFICATION) ||
        notification.uint(64).toString() !== queryId) return null;
    notification.coins();
    return notification.address();
  }

  function decodeTonswapLiquidity(transactions) {
    const rows = [];
    let incomplete = false;
    for (const tx of transactions) {
      if (!Array.isArray(tx?.outMessages)) {
        incomplete = true;
        continue;
      }
      if (tx.status !== 'success') continue;
      const events = new Map();
      let invalid = false;
      for (const message of tx.outMessages) {
        if (message?.op !== LIQUIDITY_EVENT || message.destination != null) continue;
        try {
          const event = liquidityEvent(message.body);
          const key = [event.kind, event.queryId, event.owner].join(':');
          const previous = events.get(key);
          if (previous) {
            previous.t3 += event.t3;
            previous.asset += event.asset;
          } else {
            events.set(key, { ...event, key, hash: tx.hash, utime: tx.utime });
          }
        } catch {
          invalid = true;
          break;
        }
      }
      if (invalid) {
        incomplete = true;
        continue;
      }
      for (const event of events.values()) {
        let payer = null;
        try {
          if (event.kind === 1) payer = depositPayer(tx.inMessage, event.queryId);
        } catch {
          // The event still establishes the position owner if the payer cannot be decoded.
        }
        rows.push({ ...event, payer });
      }
    }
    return { rows, incomplete };
  }

  window.decodeTonswapLiquidity = decodeTonswapLiquidity;
  window.TonswapBoc = { readCells, reader };
})();
