/*
  A bunch of handy helper functions and classes.
 */
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.

  ___FOOTGUN___: This isn't cryptographically secure.  It's mostly just used
  for generating random CSS ids and ___not___ for anything secure.
 */
export const rand_int = (min, max) => {
  return Math.floor((Math.random() * (max - min)) + min);
}

/*
  Partially taken from [a blog by Lea Verou](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.

  The main reason I use this is to block some processing until a later event happens,
  but that later thing happens randomly or in some other part of the code.  Another
  great place for `defer()` is when you have an old callback style system and you
  need to wait for it in a separate `async/await` function.

  For example, if you want to wait on a resource to be available in the browser, then
  you can create a `defer()`, grab the resource, and anything that needs this resource
  can wait on the `defer()` to know it's ready.  As an example, pretend the `setTimeout`
  in this code is a video downloading:

  ```javascript
  const video_promise = defer("blocker");

  // pretend this is loading a video
  setTimeout(() => blocker.resolve(), 3000);

  const play_video => async() => {
    await video_promise;
    // do some stuff after the video is ready
  }

  onMount(() => await play_video());
  ```

  In this code we don't want to play the video until it's totally ready (which
  we're pretending to do with a `setTimeout`).  Rather than some crazy await
  callback setup we just have a single `video_promise`, wait on it in `play_video`,
  and whatever does the video loding resolves it.

  ### Rejection

  When you call `reject` on the `defer` it will trigger an exception just like with
  a `Promise.reject`.  So the above code can be:

  ```javascript
  const play_video => async() => {
    try {
      await video_promise;
      // do some stuff after the video is ready
    } catch(error) {
      console.error(error);
    }
  }
  ```

  If the video loading code has a problem it can _signal this_ by rejecting the
  `defer`.  That will trigger error handling with simple `try/catch` semantics.

  + `debug string` -- A debug message to print out whenever this is resolved or rejected.
  + ___return___ `Promise` -- A normal `Promise`, but now it's `reject` and `resolve` callbacks are attached as methods you can call.
 */
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;
}


/*
  This implements a very simple Mutex style blocker for coordinating multiple
  async actors waiting on a single resource.  The best example is in the
  `client/components/Source.svelte` where a `Mutex` is used so that mutliple
  pages are blocked until an external slow JavaScript source is loaded.

  This hardly never comes up in JavaScript, but when it does you really need
  something like this.  In the `Source.svelte` code we want to load an external
  `.js` code, but multiple pages might try to load it at the same time. The
  solution is to:

  1. Keep track of what sources have already loaded.
  2. Use a single `Mutex` all `Source.svelte` objects use in a `context="module"`.
  3. When `Source.svelte` runs it checks to see if the code is already loaded after calling `lock.hold()`. This makes sure that it's waited its turn to try to load the source.
  4. If it is ready then it immediately releases the lock.
  5. If it hasn't been loaded yet then it injects the `<script>` tags and creates an `onLoad` handler to wait for the script to load.
  6. The `onLoad` handler then calls `lock.release()` which frees up all of the other pages waiting.
  7. All of the other pages then exit their `lock.hold()`, see that this source is open, and do their own `lock.release()` and move on.
  8. The final trick is a call `lock.wait()` which just does a quick `lock.hold()/lock.release()` combo.  This makes the JavaScript engine "bounce" to another waiting caller in the queue.

  As I said, this rarely comes up, but the situation of trying to have multiple pages on an SPA use an
  external JavaScript file fits the situation exactly.  It's multiple actors (pages) attempting to load
  the same slow randomly loading resource (a `.js` file).

  ### Why Not `<script>` Defer or Async

  The `defer` and `async` attributes to a `<script>` tag do _not_ solve the
  problem of requiring an external `.js` file be _completely ready_ before
  other code runs in an SPA.  Both keywords come from the era when scripts ran
  in the context of a single "page" and were bounded by a full HTTP request
  download.  In that context, these features work to determine when a script
  loads since it's triggered when the user transitions to a new page using a
  full HTTP request.

  In an SPA these don't work because "page" is simply a visual construct and
  not bounded by an HTTP request.  If a user clicks a link in an SPA and sees a
  new "page" there is probably no actual HTTP transition that loads it, and the
  HTTP request is only when the _whole application_ loads.  That means when you
  put a `<script>` at the top of the SPA it will load for all pages even if a
  user never hits a page needing that code.

  In an SPA like the Svelte app what you want is an ability to say, "Only try
  to load the gigantic HLS.js module when the user views a page with Videos on
  it."  With classic `<script>` tags you'll download all 300kb of the `HLS.js`
  code right when someone tries to log in and isn't even seeing a video.  In
  the world of Google using its monopoly to punish anyone with "slow" load
  times this is a death to your website.

  The `Mutex` helps the `Source.svelte` component load code only when it's
  actually needed, which improves the load times of the whole application, and
  makes it nicer for users.  Instead of waiting for a massive 500MB
  "application" to download they get a quick initial download for the majority
  of the application and then it gradually adds more code as they encounter more features.


  ___WARNING___: I think this is right but please let me know if you find bugs or have improvements
  to this code.
 */
export class Mutex {

  /*
    Not much to it, just make the Mutex.
  */
  constructor() {
    this.waiting = [];
  }

  /*
    Get the count of actors waiting on this mutex.  It's a get attribute.
  */
  get count() {
    return this.waiting.length;
  }

  /*
    This will attempt the lock, and if it can't get one then it ___does not block___.

    + ___return___ `boolean` -- If you got the lock then it's `true`, otherwise it's `false`.
   */
  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;
    }
  }

  /*
    Grab the lock, and if you're the winner then you get a `true`.  If you have to wait
    then it's a Promise.  You `await m.hold()` and then wait on the Promise, or just pass
    through if you're the winner.  Later, as actors call `release()` your Promise is popped
    off the list of waiting actors and when it's your turn your Promise is resolved and you
    continue on.  The `Promise` is then `resolve(true)` so you always receive a `true` result.

    + ___return___ `boolean|Promise` -- If you await you either get the boolean or wait.
   */
  async hold() {
    if(this.waiting.length === 0) {
      this.waiting.push(defer());
      return true;
    } else {
      const lock = defer();
      this.waiting.push(lock);
      return lock;
    }
  }

  /*
    This pops the next waiting actor, then calls `resolve(true)` on the promise.
    If you've called `hold()` then you need to call `release()` at some point or
    everyone will be blocked.
   */
  release() {
    const lock = this.waiting.pop();
    lock.resolve(true);
  }

  /*
    This effectively "context switches" so other waiting actors can go.
    It causes the caller to call `hold()` and then `release()`.  This means
    when you call this you'll push your Promise on the list, then other
    actors will go, and when they're all done, your release will cause
    the next one to go after you.
   */
  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;
}


/*
  A Svelte `use:` handler that adds a `visible` and `hidden` callback to a
  Svelte node. Very useful in forever scroll UIs.  It uses the
  `IntersectionObserver` to do the main work, then triggers a `visible` or
  `hidden` events on the node.

  ### Usage

  You use this as a Svelte `use` function to add the new events like this:

  ```html
  <script>
  let visible = false;
  const show = () => visible = true;
  const hide = () => visible = false;
  </script>

  <div use:visibility={ options } on:visible={ show } on:hidden={ hide }>
    {#if visible}
    Shown!
    {:else}
    Hidden!
    {/if}
  </div>
  ```

  In this example (from `client/components/IsVisible.svelte`) you simply have
  some callbacks that change a variable and Svelte then displays the correct
  text.  It's also used in `client/components/SnapImage.svelte`.
 */
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);
}

/*
  Similar to `visibility()` it's a Svelte `use:` function that adds
  resize detection to a node.  Not really used in my code, but this
  comes up whenever you _must_ maintain an aspect ratio and CSS simply
  can't pull it off.  It will be slow though, and use a lot of CPU, but
  sometimes that's all you can do.
 */
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);
}
