<script>
  import { onMount, onDestroy } from 'svelte';
  import Spinner from "$/client/components/Spinner.svelte";
  import Countdown from "$/client/components/Countdown.svelte";
  import Icon from '$/client/components/Icon.svelte';
  import FSM from '$/client/fsm.js';
  import assert from "$/client/assert.js";
  import { log } from "$/client/logging.js";

  export let aspect_ratio = "16/9";
  export let source = "";
  export let content_type = "application/x-mpegURL";
  export let poster = "";
  export let preload = "";
  export let playsinline = false;
  export let poll_tick = 10000;
  export let poll_limit = 10;
  export let background_color = "var(--color-accent)";
  export let starts_on = false;
  export let spinner = true;
  export let controls = true;
  export let muted = false;
  export let autoplay = false;
  let video_config = {video_ready: false};

  let poll_count = 0;
  let poll_timer;
  let fsm;
  let hls;
  export let video_tag;

  const supports_hls_native = () => {
    let video = document.getElementById("video-player");
    return video.canPlayType('application/vnd.apple.mpegURL') != "" || video.canPlayType('application/x-mpegURL') != "";
  }

  const load_hlsjs_script = () => {
    let script = document.createElement("script");
    script.setAttribute('src', "/js/hls.js");
    document.body.appendChild(script);
    script.onload = async () => {
      // NOTE: might have to revisit this as it might not reliably keep hls.js loaded
      if(!video_config.hlsjs_configured) {
        video_config.hlsjs_configured = true;
        await fsm.do("hlsjs_loaded");
      }
    }
  }

  /* You pass it the state it should return on success, will
   * return HLS_ERROR if there's a problem.
   */
  const recover_hls = (state) => {
    log.debug("HLS error", video_config.hls_error_type, "in state", state);
    switch(video_config.hls_error_type) {
      case Hls.ErrorTypes.NETWORK_ERROR:
        hls.startLoad();
        return state;
      case Hls.ErrorTypes.MEDIA_ERROR:
        hls.recoverMediaError();
        return state;
      default:
        log.debug("HLS error is unknown type");
        hls.destroy();
        return "HLS_ERROR";
    }
  }

  const check_video = async () => {
    let test = await fetch(source);
    poll_count += poll_count;

    if(test.status == 200) {
      await fsm.do("video_ready");
      clearInterval(poll_timer);
    } else if(poll_count > poll_limit) {
      clearInterval(poll_timer);
      await fsm.do("poll_limit");
    }

    return test.status == 200;
  }

  const poll_video_ready = async () => {
    if(!await check_video()) {
      poll_timer = setInterval(check_video, poll_tick);
    }
  }

  const start_hlsjs = () => {
    assert(source.includes(".m3u8"), `HLS is loaded but source doesn't have m3u8 "${source}"`);
    assert(Hls !== undefined, "hlsjs_configured is true but no Hls object?");
    assert(Hls.isSupported(), "HLS trying to run but Hls.isSupported() claims false.");

    let video = document.getElementById("video-player");
    hls = new Hls();
    hls.attachMedia(video);
    hls.on(Hls.Events.MEDIA_ATTACHED, () => {
      log.debug("HLS MEDIA_ATTACHED event start.");
      hls.loadSource(source);
      hls.on(Hls.Events.MANIFEST_PARSED,
        async (event, details) => {
          log.debug("HLS MANIFEST_PARSED event start.");
          await fsm.do("video_ready")
        });

      hls.on(Hls.Events.ERROR, async (event, error) => {
        log.debug("HLS ERROR event start.", error.type);
        video_config.hls_error_type = error.type;
        await fsm.do("hls_error");
      });

    });
  }

  const detect_video = () => {
    if(!video_config.hls_source || video_config.hls_natively) {
      // can just use HTML5 video directly
      return ["POLLING_VIDEO", poll_video_ready];
    } else if(video_config.hlsjs_configured) {
      // it's an HLS video but the browser doesn't support it
      // the hls.js script tag is already loaded so just run it
      return ["HLSJS_READY", start_hlsjs];
    } else {
      // still an HLS video, but need to load hls.js
      // need to load the hls.js script tag for this browser
      return ["LOADING_HLSJS", load_hlsjs_script];
    }
  }


  class VideoEvents {
    async mount(state) {
      switch(state) {
        case "START":
          video_config.hls_source = source.includes(".m3u8");
          video_config.hls_natively = supports_hls_native();

          if(starts_on && !video_config.countdown_done) {
            return "COUNTING_DOWN";
          } else {
            return detect_video();
          }
        default:
          return "ERROR";
      }
    }

    async hlsjs_loaded(state) {
      switch(state) {
        case "LOADING_HLSJS":
          start_hlsjs();
          return "HLSJS_READY";
        default:
          return "ERROR";
      }
    }

    async video_ready(state) {
      switch(state) {
        case "VIDEO_READY":
        case "POLLING_VIDEO":
        case "HLSJS_READY":
        case "HLS_RECOVERING":
        case "HLS_POLLING":
          video_config.video_ready = true;
          return "VIDEO_READY";
        default:
          return "ERROR";
      }
    }

    async hls_error(state) {
      // attempt to just resolve the error and stay in the same state
      // TODO: this might be wrong, need to confirm it. Previously I
      // return "HLS_RECOVERING" instead of the state that was given
      switch(state) {
        case "HLS_RECOVERING":
          return recover_hls(state);
        case "VIDEO_READY":
          return recover_hls(state);
        default:
          log.debug("HLS error on state", state, "Not handled");
          return "ERROR";
      }
    }

    countdown_done(state) {
      video_config.countdown_done = true;
      switch(state) {
        case "COUNTING_DOWN":
          return detect_video();
        default:
          return "ERROR";
      }
    }

    poll_limit(state) {
      switch(state) {
        case "POLLING_VIDEO":
          return "POLL_LIMIT";
        default:
          return "ERROR";
      }
    }

    clean_video() {
      // this does seem to help with ram on Safari but only just
      video_tag.pause();
      video_tag.src = "";
      video_tag.load();
    }

    destroy(state) {
      log.debug("FSM: Destroying video on state", state);
      if(hls) hls.destroy();

      if(video_tag) this.clean_video();

      clearInterval(poll_timer)
      video_config.video_ready = false;
      video_config.hls_error_type = undefined;
      return "END";
    }
  }

  onDestroy(async () => await fsm.do("destroy"));

  onMount(async () => {
    await fsm.do("mount");
  });


  fsm = new FSM(video_config, new VideoEvents());
  fsm.onTransition(fsm => {
    video_config.state = fsm.state
  });

</script>

<style>
  video-player video.not-ready {
    filter: blur(4px);
  }

  video-player loading-panel {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }

  video-player loading-panel:hover {
    box-shadow: var(--box-shadow) var(--color-shadow);
  }

  video-player {
    overflow: hidden;
  }

  video-player loading-panel h1,
  video-player loading-panel h2 {
    color: var(--color-bg-secondary);
    font-size: 7vw;
    margin-top: 0;
    margin-bottom: 0;
    text-shadow: 8px 8px 5px var(--color-overlay-shadow);
  }

  video-player loading-panel h2 {
    font-size: 4vw;
  }
</style>

<!-- Safari leaks the video content if you use a <source> tag. Future versions of this will
  try to detect Safari and adjust accordingly, but for now this is the only thing that works.
-->
<video-player class="stacked" style="background: { background_color };">
  <video bind:this={ video_tag }
         playsinline={ playsinline }
         src={ source }
         type={ content_type }
         class:not-ready={video_config.state !== "VIDEO_READY"}
         class="layer" style="--aspect-ratio: { aspect_ratio };"
         poster={poster}
         controls={ controls && video_config.video_ready }
         id="video-player"
         preload={ preload }
         autoplay={ autoplay }
         muted={ muted }
         >
  </video>

  {#if video_config.state !== "VIDEO_READY"}
    <loading-panel class="layer top">
      {#if video_config.state === "COUNTING_DOWN" }
        <Countdown starts_on={ starts_on } on:done={ () => fsm.do("countdown_done") } />
      {:else if video_config.state === "POLL_LIMIT" }
        <Icon name="alert-triangle" size="7vw" color="var(--color-bg-secondary)" shadow={ true } />
        <h1>Video Broken</h1>
        <h2>Come back in a few minutes...</h2>
      {:else if spinner}
        <Spinner color="var(--color-bg)" size="128" />
      {:else}
        <img src={ poster } />
      {/if}
    </loading-panel>
  {/if}
</video-player>
