text: import log from '../log.js'
  import { requestVideoStream, requestDisableVideo } from './camera.js'
  
  /**
   * Video Manager for video extensions.
   */
  class VideoProvider {
    constructor() {
      /**
       * Default value for mirrored frames.
       * @type boolean
       */
      this.mirror = true
  
      /**
       * Cache frames for this many ms.
       * @type number
       */
      this._frameCacheTimeout = 16
  
      /**
       * DOM Video element
       * @private
       */
      this._video = null
  
      /**
       * Usermedia stream track
       * @private
       */
      this._track = null
  
      /**
       * Stores some canvas/frame data per resolution/mirror states
       */
      this._workspace = []
    }
  
    static get FORMAT_IMAGE_DATA() {
      return 'image-data'
    }
  
    static get FORMAT_CANVAS() {
      return 'canvas'
    }
  
    /**
     * Dimensions the video stream is analyzed at after its rendered to the
     * sample canvas.
     * @type {Array.<number>}
     */
    static get DIMENSIONS() {
      return [480, 360]
    }
  
    /**
     * Order preview drawable is inserted at in the renderer.
     * @type {number}
     */
    static get ORDER() {
      return 1
    }
  
    /**
     * Get the HTML video element containing the stream
     */
    get video() {
      return this._video
    }
  
    /**
     * Request video be enabled.  Sets up video, creates video skin and enables preview.
     *
     * @return {Promise.<Video>} resolves a promise to this video provider when video is ready.
     */
    enableVideo() {
      this.enabled = true
      return this._setupVideo()
    }
  
    /**
     * Disable video stream (turn video off)
     */
    disableVideo() {
      this.enabled = false
      // If we have begun a setup process, call _teardown after it completes
      if (this._singleSetup) {
        this._singleSetup
          .then(this._teardown.bind(this))
          .catch((err) => this.onError(err))
      }
    }
  
    /**
     * async part of disableVideo
     * @private
     */
    _teardown() {
      // we might be asked to re-enable before _teardown is called, just ignore it.
      if (this.enabled === false) {
        const disableTrack = requestDisableVideo()
        this._singleSetup = null
        // by clearing refs to video and track, we should lose our hold over the camera
        this._video = null
        if (this._track && disableTrack) {
          this._track.stop()
        }
        this._track = null
      }
    }
  
    /**
     * Return frame data from the video feed in a specified dimensions, format, and mirroring.
     *
     * @param {object} frameInfo A descriptor of the frame you would like to receive.
     * @param {Array.<number>} frameInfo.dimensions [width, height] array of numbers.  Defaults to [480,360]
     * @param {boolean} frameInfo.mirror If you specificly want a mirror/non-mirror frame, defaults to true
     * @param {string} frameInfo.format Requested video format, available formats are 'image-data' and 'canvas'.
     * @param {number} frameInfo.cacheTimeout Will reuse previous image data if the time since capture is less than
     *                                        the cacheTimeout.  Defaults to 16ms.
     *
     * @return {ArrayBuffer|Canvas|string|null} Frame data in requested format, null when errors.
     */
    getFrame({
      dimensions = VideoProvider.DIMENSIONS,
      mirror = this.mirror,
      format = VideoProvider.FORMAT_IMAGE_DATA,
      cacheTimeout = this._frameCacheTimeout,
    }) {
      if (!this.videoReady) {
        return null
      }
      const [width, height] = dimensions
      const workspace = this._getWorkspace({
        dimensions,
        mirror: Boolean(mirror),
      })
      const { videoWidth, videoHeight } = this._video
      const { canvas, context, lastUpdate, cacheData } = workspace
      const now = Date.now()
  
      // if the canvas hasn't been updated...
      if (lastUpdate + cacheTimeout < now) {
        if (mirror) {
          context.scale(-1, 1)
          context.translate(width * -1, 0)
        }
  
        context.drawImage(
          this._video,
          // source x, y, width, height
          0,
          0,
          videoWidth,
          videoHeight,
          // dest x, y, width, height
          0,
          0,
          width,
          height
        )
  
        // context.resetTransform() doesn't work on Edge but the following should
        context.setTransform(1, 0, 0, 1, 0, 0)
        workspace.lastUpdate = now
      }
  
      // each data type has it's own data cache, but the canvas is the same
      if (!cacheData[format]) {
        cacheData[format] = { lastUpdate: 0 }
      }
      const formatCache = cacheData[format]
  
      if (formatCache.lastUpdate + cacheTimeout < now) {
        if (format === VideoProvider.FORMAT_IMAGE_DATA) {
          formatCache.lastData = context.getImageData(0, 0, width, height)
        } else if (format === VideoProvider.FORMAT_CANVAS) {
          // this will never change
          formatCache.lastUpdate = Infinity
          formatCache.lastData = canvas
        } else {
          log.error(`video io error - unimplemented format ${format}`)
          // cache the null result forever, don't log about it again..
          formatCache.lastUpdate = Infinity
          formatCache.lastData = null
        }
  
        // rather than set to now, this data is as stale as it's canvas is
        formatCache.lastUpdate = Math.max(
          workspace.lastUpdate,
          formatCache.lastUpdate
        )
      }
  
      return formatCache.lastData
    }
  
    /**
     * Method called when an error happens.  Default implementation is just to log error.
     *
     * @abstract
     * @param {Error} error An error object from getUserMedia or other source of error.
     */
    onError(error) {
      log.error('Unhandled video io device error', error)
    }
  
    /**
     * Create a video stream.
     * @private
     * @return {Promise} When video has been received, rejected if video is not received
     */
    _setupVideo() {
      // We cache the result of this setup so that we can only ever have a single
      // video/getUserMedia request happen at a time.
      if (this._singleSetup) {
        return this._singleSetup
      }
  
      this._singleSetup = requestVideoStream({
        width: { min: 480, ideal: 640 },
        height: { min: 360, ideal: 480 },
      })
        .then((stream) => {
          this._video = document.createElement('video')
  
          // Use the new srcObject API, falling back to createObjectURL
          try {
            this._video.srcObject = stream
          } catch (error) {
            this._video.src = window.URL.createObjectURL(stream)
          }
          // Hint to the stream that it should load. A standard way to do this
          // is add the video tag to the DOM. Since this extension wants to
          // hide the video tag and instead render a sample of the stream into
          // the webgl rendered Scratch canvas, another hint like this one is
          // needed.
          this._video.play() // Needed for Safari/Firefox, Chrome auto-plays.
          this._track = stream.getTracks()[0]
          return this
        })
        .catch((error) => {
          this._singleSetup = null
          this.onError(error)
        })
  
      return this._singleSetup
    }
  
    get videoReady() {
      if (!this.enabled) {
        return false
      }
      if (!this._video) {
        return false
      }
      if (!this._track) {
        return false
      }
      const { videoWidth, videoHeight } = this._video
      if (typeof videoWidth !== 'number' || typeof videoHeight !== 'number') {
        return false
      }
      if (videoWidth === 0 || videoHeight === 0) {
        return false
      }
      return true
    }
  
    /**
     * get an internal workspace for canvas/context/caches
     * this uses some document stuff to create a canvas and what not, probably needs abstraction
     * into the renderer layer?
     * @private
     * @return {object} A workspace for canvas/data storage.  Internal format not documented intentionally
     */
    _getWorkspace({ dimensions, mirror }) {
      let workspace = this._workspace.find(
        (space) =>
          space.dimensions.join('-') === dimensions.join('-') &&
          space.mirror === mirror
      )
      if (!workspace) {
        workspace = {
          dimensions,
          mirror,
          canvas: document.createElement('canvas'),
          lastUpdate: 0,
          cacheData: {},
        }
        workspace.canvas.width = dimensions[0]
        workspace.canvas.height = dimensions[1]
        workspace.context = workspace.canvas.getContext('2d')
        this._workspace.push(workspace)
      }
      return workspace
    }
  }
  
  export default VideoProvider
  