'use strict';

function throttle(fn, ms) {
  let lastCall = 0;
  let timeoutId = null;
  let pendingArgs = null;
  let pendingThis = null;
  const invoke = () => {
    lastCall = Date.now();
    timeoutId = null;
    if (pendingArgs) {
      const args = pendingArgs;
      const ctx = pendingThis;
      pendingArgs = null;
      pendingThis = null;
      fn.apply(ctx, args);
    }
  };
  const throttled = function (...args) {
    pendingArgs = args;
    pendingThis = this;
    const now = Date.now();
    const elapsed = now - lastCall;
    if (elapsed >= ms) {
      if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
      invoke();
    } else if (!timeoutId) {
      timeoutId = setTimeout(invoke, ms - elapsed);
    }
  };
  throttled.flush = () => {
    if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
    if (pendingArgs) invoke();
  };
  throttled.cancel = () => {
    if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
    pendingArgs = null;
    pendingThis = null;
  };
  return throttled;
}

class Coalescer {
  constructor(emit, ms) {
    this.emit = emit;
    this.ms = ms;
    this.buffer = [];
    this.timer = null;
  }
  push(item) {
    this.buffer.push(item);
    if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.ms);
    }
  }
  flush() {
    if (this.timer) { clearTimeout(this.timer); this.timer = null; }
    if (this.buffer.length === 0) return;
    const items = this.buffer;
    this.buffer = [];
    this.emit(items);
  }
}

module.exports = { throttle, Coalescer };
