export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number,
  immediate = false
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout> | null;

  return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
    const context = this;
    const callNow = immediate && !timeout;

    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) fn.apply(context, args);
    }, delay);

    if (callNow) fn.apply(context, args);
  };
}

export function throttle<T extends (...args: any[]) => any>(
  fn: T,
  delay: number,
  immediate = false
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;

  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    const context = this;

    if (!timer) {
      if (immediate) {
        fn.apply(context, args);
      } else {
        lastArgs = args;
      }

      timer = setTimeout(() => {
        if (!immediate && lastArgs) {
          fn.apply(context, lastArgs);
          lastArgs = null;
        }
        timer = null;
      }, delay);
    }
  };
}

export function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache: Map<string, ReturnType<T>> = new Map();

  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log("Returning from cache:", key);
      return cache.get(key)!;
    }
    const result = fn.apply(this, args);
    cache.set(key, result);

    console.log("Calculating new result:", key);
    return result;
  } as T;
}

export const asyncMemoize = async <T extends (...args: any[]) => Promise<any>>(
  fn: T
): Promise<T> => {
  const cache = new Map<string, any>();

  return async function (...args: Parameters<T>) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = await fn(...args);
    cache.set(key, result);
    return result;
  } as T;
};

export class OperationBatcher {
  private operations: (() => Promise<any>)[] = [];
  private executing = false;

  enqueueOperation(fn: () => Promise<any>) {
    this.operations.push(fn);
    if (!this.executing) {
      this.execute();
    }
  }

  private async execute() {
    this.executing = true;
    while (this.operations.length > 0) {
      const operation = this.operations.shift();
      if (operation) {
        try {
          await operation();
        } catch (err) {
          console.error("Operation failed", err);
        }
      }
    }
    this.executing = false;
  }
}

export function runCallbackOnUserEvent(
  callback: () => void,
  options?: {
    events?: (keyof DocumentEventMap)[];
    timeout?: number;
  }
): void {
  if (typeof callback !== "function") {
    throw new Error("Callback must be a function");
  }

  const { events = ["mousemove", "scroll", "touchstart"], timeout } =
    options || {};

  let executed = false;

  function runOnce(): void {
    if (executed) return;
    executed = true;

    for (const event of events) {
      document.removeEventListener(event, runOnce);
    }

    callback();
  }

  for (const event of events) {
    document.addEventListener(event, runOnce, { once: true, passive: true });
  }

  if (typeof timeout === "number" && timeout > 0) {
    setTimeout(runOnce, timeout);
  }
}

export function loadScriptOnUserEvent(
  src: string,
  options?: {
    events?: (keyof DocumentEventMap)[];
    timeout?: number;
  }
): void {
  if (typeof src !== "string" || !src.trim()) {
    throw new Error("Script source must be a non-empty string");
  }

  const { events = ["mousemove", "scroll", "touchstart"], timeout } =
    options || {};

  let executed = false;

  function loadScript(): void {
    if (executed) return;
    executed = true;

    for (const event of events) {
      document.removeEventListener(event, loadScript);
    }

    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    document.head.appendChild(script);
  }

  for (const event of events) {
    document.addEventListener(event, loadScript, { once: true, passive: true });
  }

  if (typeof timeout === "number" && timeout > 0) {
    setTimeout(loadScript, timeout);
  }
}

export function observeElementOnIntersect(
  selector: string,
  options: IntersectionObserverInit,
  callback: (entry: IntersectionObserverEntry) => void
): void {
  if (typeof selector !== "string") {
    throw new Error("Selector must be a string");
  }

  const elements = document.querySelectorAll<HTMLElement>(selector);
  if (!elements.length) return;

  const observer = new IntersectionObserver((entries, obs) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        callback(entry);
        obs.unobserve(entry.target);
      }
    }
  }, options);

  elements.forEach((el) => observer.observe(el));
}


type BreakpointRange = {
  min: number;
  max: number;
};

type Breakpoints = Record<string, BreakpointRange>;

export function getScreenSize(
  ranges: Breakpoints = {
    xs: { min: 0, max: 480 },
    sm: { min: 481, max: 640 },
    md: { min: 641, max: 768 },
    lg: { min: 769, max: 1024 },
    xl: { min: 1025, max: 1280 },
    "2xl": { min: 1281, max: 1536 },
    "3xl": { min: 1537, max: Infinity }
  }
): string | undefined {
  const width = window.innerWidth;

  for (const [label, { min, max }] of Object.entries(ranges)) {
    if (width >= min && width <= max) {
      return label;
    }
  }

  return undefined; // fallback
}

export function watchScreenSize(
  callback: (size: string | undefined) => void,
  ranges?: Breakpoints
): () => void {
  function handler() {
    const size = getScreenSize(ranges || undefined);
    callback(size);
  }

  window.addEventListener('resize', handler);
  handler(); // Call immediately on setup

  return () => window.removeEventListener('resize', handler);
}

