import { log } from "./logging.js";

/* A simple function that uses the Crypto.getRandomValues to fill an
 * array with 32 bit random numbers.
 */
export const random_numbers = (count) => {
  let out = new Uint32Array(count);
  return window.crypto.getRandomValues(out);
}

/* The stupid web crypto api doesn't have the randomInt function that the
 * node crypto library has so I have to hack in this complete BS.  I really
 * hate browser manufacturers.
 */
export const rand_int = (min, max) => {
  return Math.floor((Math.random() * (max - min)) + min);
}

/**
 * Partially taken from https://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/ on
 * how to externally resolve a promise.  You can use this in the very common situation in svelte where you need
 * to await on some condition, but you can't create the promise until the onMount. Instead start it off with defer()
 * then resolve it when you're ready.
 */
export const defer = (debug="") => {
  let res;
  let rej;

  let promise = new Promise((resolve, reject) => {
    if(debug) {
      res = (result) => {
        log.debug("resolved defer", debug);
        resolve(result);
      }

      rej = (error) => {
        log.debug("REJECT defer", debug);
        reject(error);
      }
    } else {
      res = resolve;
      rej = reject;
    }
  });

  promise.resolve = res;
  promise.reject = rej;

  return promise;
}

export class Mutex {

  constructor() {
    this.waiting = [];
  }

  get count() {
    return this.waiting.length;
  }

  async attempt() {
    if(this.waiting.length === 0) {
      this.waiting.push(defer());
      return true;
    } else {
      // they don't want to get it so return false
      return false;
    }
  }

  async hold() {
    if(this.waiting.length === 0) {
      this.waiting.push(defer());
      return true;
    } else {
      const lock = defer();
      this.waiting.push(lock);
      return lock;
    }
  }

  release() {
    const lock = this.waiting.pop();
    lock.resolve(true);
  }

  async wait() {
    await this.hold();
    await this.release();
  }
}

/**
 * Used inside observer actions (see Svelte actions) to create a
 * cleanup function when the action is destroyed.  This by default
 * calls unobserve on the node, but it will also do a callback if
 * provided to options.  Callbacks are:
 *
 * update(node, opts) - Called whenever the values of options is updated, includes the node.
 * destroy(node) - Called when the action is destroyed, includes the node.
 *
 */
const create_observer_cleanup = (observer, node, options) => {
  const cleaner = {
    destroy() {
      if(options.destroy) options.destroy(node);
      observer.unobserve(node);
    }
  }

  if(options.update) {
    cleaner.update = (opts) => {
      options.update(node, opts);
    }
  }

  return cleaner;
}


export const visibility = (node, opts) => {
  let options = opts || {}; // NOTE: does JS default values have the ruby problem?
  const callback = (entries) => {
    for(let entry of entries) {
      if(entry.isIntersecting) {
        node.dispatchEvent(new CustomEvent("visible", { detail: entry }));
      } else {
        node.dispatchEvent(new CustomEvent("hidden", { detail: entry }));
      }
    }
  }

  const vis_observer = new IntersectionObserver(callback, options || {});
  vis_observer.observe(node);

  return create_observer_cleanup(vis_observer, node, options);
}

export const resize = (node, options) => {
  const callback = (entries) => {
    entries.forEach(entry => {
      node.dispatchEvent(new CustomEvent("resize", { detail: entry }));
    });
  }

  const resize_observer = new ResizeObserver(callback, options || {});

  resize_observer.observe(node);

  return create_observer_cleanup(resize_observer, node, options);
}
