/*
  This needs an entire rewrite using the FSM code.  Most of this is hacked on
  garbage that is unreliable at the best of times.  The documentation here is
  mostly give what I have a bit of help, and I keep this _mostly_ as an example
  of how to work with an older API.  In the future I want to rewrite this--or write
  an alternative--that uses the `client/fsm.js` library similar to how `HLSVideo.svelte`
  works.
 */

const seedTimeout = 60000;
import { webtorrent, base_host } from '$/client/config.js';
import assert from "$/client/assert.js";
import { log } from "$/client/logging.js";


let LAZY_CLIENT;  // this is lazy loaded by getClient

/*
  Load the massive WebTorrent client javascript.

  ___WARNING___: having svelte build the webtorrent client into the script
  makes it slow to load and massive.  it's much faster to set it as a script
  variable in the index.html but that might cause loading problems WebTorrent
  then comes from the public/index.html file, not from an import in here
 */
const getClient = () => {
  // super hacks!
  if(LAZY_CLIENT) {
    return LAZY_CLIENT;
  } else {
    // eslint will complain but webtorrent is loaded by <script>
    let client = new WebTorrent(webtorrent);

    // would need to do some kind of global status for these since it's not per torrent
    if(webtorrent.rate !== undefined) {
      client.throttleDownload(webtorrent.rate);
      client.throttleUpload(webtorrent.rate);
    }

    client.on('error', err => log.error(err));
    client.on('listening', () => log.debug('LISTENING:'));
    LAZY_CLIENT = client;
    return LAZY_CLIENT;
  }
}

/*
  Base class for handling WebTorrent events.  Look at the `VideoEvents` class
  in `client/components/WTVideo.svelte` for how to use it.  You subclass this
  if you want to give visual feedback to different events in WebTorrent, such
  as new clients connecting, download rates, etc.
 */
export class WTEvents {
  /*
    Takes a `Media` object and video player options, then sets up the
    necessary things to make a video play after WebTorrent has downloaded it.

    + `media Media` -- Media object from the database.
    + `video_opts Object` -- Default is `{autoplay: false, controls: true, muted: false}`.
   */
  constructor(media, video_opts) {
    assert(media, "Media must exist.");
    this.media = media;
    this.video_opts = video_opts || {autoplay: false, controls: true, muted: false}
    this.appendId = `append-${media.target_id}`;
  }

  /* Set the torrent to use. */
  setTorrent(torrent) {
    // TODO: refine this
    this.media.torrent = torrent;
    this.torrent = torrent;
  }

  /*
    Does the work needed when a video is ready.
    Be sure to call it with super if you override.
   */
  videoReady(file) {
    this.media.kind = 'video';
    file.renderTo(`#${this.appendId}`, this.video_opts);
    file.getBlobURL((err, url) => {
      if(err) log.error(err, "getBlobURL error videoReady");
      this.download_available(url);
    });
  }

  /*
    Same as videoReady. This will setup HTML5 `<audio>` tags instead
    of `<video>` tags.
   */
  audioReady(file) {
    this.media.kind = 'audio';
    file.renderTo(`#${this.appendId}`, this.video_opts);
    file.getBlobURL((err, url) => {
      if(err) log.error(err, "getBlobURL error audioReady");
      this.download_available(url);
    });
  }

  /*
    Other kinds of media that WebTorrent will download. This doesn't
    reall do much other than call `this.download_available()`. You
    probably just want to override `download_available()` then
    use that to make a download link.
   */
  otherReady(file) {
    file.renderTo(`#${this.appendId}`, (err, elem) => {
      // TODO: hmmm do we need to do anything here with elem?
      if(err) {
        log.error(err);
      } else {
        // keep this active for 60 second
        let client = getClient();
        setTimeout(() => client.remove(this.torrent.infoHash), seedTimeout);
      }
    });

    file.getBlobURL((err, url) => {
      if(err) log.error(err, "getBlobURL error");
      this.download_available(url);
    });
  }

  /* Override this to do something when a download of the media is available. */
  download_available(url) {
    log.debug("download url available");
  }

  /* Override this to handle when it's done downloading. */
  done() {
    log.debug("done", this.media);
  }

  /* Probably don't need to alter this. Use super if you override it. */
  infoHash(hash) {
    this.media.info_hash = hash;
  }

  /* Probably don't need to alter this. Use super if you override it. */
  metadata(data) {
    this.media.metadata = data;
  }

  /*
    Just returns true to WebTorrent, but you can override this if you
    want to do something before WebTorrent starts. Your version
    must return true.
   */
  ready() {
    return true;
  }

  /* Logs WebTorrent warnings. */
  warning(msg) {
    log.debug("WT WARNING", msg);
  }

  /* Logs WebTorrent errors. */
  error(msg) {
    log.debug("WT ERROR", msg);
  }

  /* Event when bytes are downloaded. Override to process it. */
  download(bytes) {
    return bytes !== undefined;
  }

  /* Event when bytes are uploaded. Override to process it. */
  upload(bytes) {
    return bytes !== undefined;
  }

  /* Notification when there's a new connection to ... a wire. */
  wire(wire) {
    // this weird idiom is to make eslint shut up for these placeholders
    return wire !== undefined;
  }

  /* WebTorrent event for ... uh... noPeers? */
  noPeers(announceType) {
    log.debug('noPeers from', announceType);
  }

  /* Handles any client errors, currently just logs. */
  client_error(err) {
    log.error(err, "client error");
    return true;
  }
}


/*
  This is called inside WebTorrent to process a new torrent being added to
  the client.  You don't really call this.
 */
const handle_torrent = (media, torrent, events) => {
  torrent.files.forEach((file) => {
    media.file_name = file.name;

    if(media.file_name.endsWith('mp4') || media.file_name.endsWith('webm')) {
      events.videoReady(file);
    } else if(media.file_name.endsWith('mp3')) {
      events.audioReady(file);
    } else {
      events.otherReady(file);
    }
  });

  // do not remove these wrapping functions.  without them you'll have the classic JS error
  // of this inside the events class being randomly set to torrent or whatever it wants.
  // You have to call them inside a closure like this to make sure events.this doesn't change.
  torrent.on('done', () => events.done());
  torrent.on('infoHash', hash => events.infoHash(hash));
  torrent.on('metadata', data => events.metadata(data));
  torrent.on('ready', () => events.ready());
  torrent.on('warning', warning => events.warning(warning));
  torrent.on('error', msg => events.error(msg));
  torrent.on('download', bytes => events.download(bytes));
  torrent.on('upload', bytes => events.upload(bytes));
  torrent.on('wire', wire => events.wire(wire));
  torrent.on('noPeers', announceType => events.noPeers(announceType));
}

/*
  Used in `client/components/WTVideo.svelte` to fetch the correct .torrent file
  for the media object.

  + `media Media` -- This is a `lib/models.js:Media` object.
 */
export const fetch_torrent_file = async (media) => {
  assert(media, "media can't be undefined");
  assert(media.torrent_url, "media does not have torrent_url set");

  media.full_torrent_url = `${base_host}${media.torrent_url}`;

  let res = await fetch(media.full_torrent_url, { credentials: "same-origin" });

  if(res.status === 200) {
    media.torrent_file = await res.blob();
    return true;
  } else {
    return false;
  }
}

/*
  This thing's job is to load the torrents with webtorrent and then fill out the active_media.

  Debug in the browser with:  localStorage.debug = '*';
 */
export const load = (media, events) => {
  assert(media, "media can't be undefined");
  assert(media.torrent_file, "media does not have torrent_file set");

  let client = getClient();

  client.add(media.torrent_file, {private: webtorrent.private, withCredentials: true}, (torrent) => {
    events.setTorrent(torrent);
    handle_torrent(media, torrent, events);
  });
}

/*
  WebTorrent does a weird internal client management thing that really
  is not friendly with an SPA.  To manage it, I have a single WebTorrent
  and then you can add or remove media to it.  This is how you remove
  any media that's active.  Use it when people transition off of a page
  showing a video so that it stops eating RAM.

  + `media Media` -- Media object from the database.
 */
export const remove = (media) => {
  if(media.torrent) {
    getClient().remove(media.torrent);
  } else {
    log.error("remove called with a media object without a torrent", media);
  }
}

export default { load, remove, fetch_torrent_file, WTEvents };
