const createLogger = pfx => {
  return {
    info: (...args) => console.info(...pfx, ...args),
    log: (...args) => console.log(...pfx, ...args),
    warn: (...args) => console.warn(...pfx, ...args),
    error: (...args) => console.error(...pfx, ...args),
  };
};

const isImage = (src) => {
  return /\w+\.(jpg|png|jpeg|svg|webp)(?=\?|$)/i.test(src);
};

const isVideo = (src) => {
  return /\w+\.(mp4|3gp|webm|ogv|avif)(?=\?|$)/i.test(src);
};

/* TODO
 * - ready promise
 **
 * types: image, [video, webcam], [canvas, shader], raw
 */
class RepaTexture extends HTMLElement {
  constructor() {
    super();
    this.logger = createLogger(["%c[repa-texture]", "background: #282828; color: #fabd2f"]);
    this.ready = false;
    this._forceUpdate = false;
  }

  connectedCallback() {
    this._load();
  }

  static get observedAttributes() {
    return ['src', 'type', 'mag-filter', 'min-filter', 'filter', 'wrap-s', 'wrap-t', 'wrap-r', 'wrap', 'format'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.logger.info(`Attribute changed: ${name} ${oldValue} -> ${newValue}`);
    if (this.ready) {
      if (name === 'src') {
        this._load(true);
      } else {
        this._forceUpdate = true;
      }
    }
  }

  async _load(reload = false) {
    if (this.ready && !reload) {
      return;
    }

    this.ready = false;

    // drop old content if we created it
    if (this._hiddenEl) {
      this._hiddenEl.remove();
      this.ref = null;
    }

    // load new ref
    const ref = this.getAttribute('ref');
    if (ref) {
      const refEl = document.getElementById(ref);
      if (refEl) {
        this.ref = refEl;
      }
    }

    if (!this.ref) {
      const src = this.getAttribute('src');
      if (src) {
        try {
          this.ref = await this._loadSource(src);
        } catch (e) {
          this.logger.error(`Source ${src} cannot be loaded:`, e);
          this.ready = true;
        }
      }
    }

    if (this.ref) {
      this.ready = true;
      this._forceUpdate = true;
    } else if (this.textContent) {
      this.simpleContent(JSON.parse(this.textContent));
    } else {
      this.logger.warn('Texture content cannot be loaded!');
    }
  }

  async _loadSource(src) {
    const type = this.getAttribute('type') || this._guessType(this.src);
    this._type = type;

    if (!type) {
      this.logger.error(`Unknown type: ${src}`);
    } else {
      this.logger.log(`Loading ${src} as ${type}`);
    }

    let ref = null;

    switch (type) {
      case 'image':
        ref = await this._loadImage(src);
      break;
      case 'video':
        ref = await this._loadVideo(src);
      break;
      case 'webcam':
        ref = await this._createWebcam();
      break;
    }

    return ref;
  }

  _hideInDOM(el) {
    const hiddenEl = document.createElement('div');
    hiddenEl.style.width = hiddenEl.style.height = '1px';
    hiddenEl.style.top = hiddenEl.style.left = '1px';
    hiddenEl.style.overflow = 'hidden';
    hiddenEl.style.position = 'fixed';
    hiddenEl.style.zIndex = '-100';
    hiddenEl.style.opacity = '0';
    hiddenEl.style.pointerEvents = 'none';
    hiddenEl.appendChild(el);

    this._hiddenEl = hiddenEl;
    document.body.appendChild(this._hiddenEl);
  }

  async _loadImage(src) {
    const imgEl = document.createElement('img');
    imgEl.crossOrigin = 'anonymous';
    const loader = new Promise((resolve, reject) => {
      imgEl.onload = () => resolve(imgEl);
      imgEl.onerror = reject;
      imgEl.src = src;
    });

    this._hideInDOM(imgEl);

    return await loader;
  }

  async _loadVideo(src) {
    const videoEl = document.createElement('video');
    videoEl.crossOrigin = 'anonymous';
    videoEl.autoplay = true;
    videoEl.loop = true;
    videoEl.muted = true;
    videoEl.playsInline = true;
    videoEl.src = src;

    this._hideInDOM(videoEl);

    return videoEl;
  }

  async _createWebcam() {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      const videoEl = document.createElement('video');
      videoEl.autoplay = true;
      videoEl.width = 640;
      videoEl.height = 480;

      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      videoEl.srcObject = stream;

      this._hideInDOM(videoEl);

      return videoEl;
    }

    this.logger.error('Webcam is not supported');

    return null;
  }

  get src() {
    return this.getAttribute('src');
  }

  get type() {
    if (this._type) {
      return this._type;
    } else if (this.hasAttribute('type')) {
      this._type = this.getAttribute('type');
      return this._type;
    }

    if (this.ref) {
      if (this.ref instanceof HTMLImageElement) {
        return 'image';
      } else if (this.ref instanceof HTMLVideoElement) {
        return 'video';
      } else if (this.ref instanceof HTMLCanvasElement) {
        return 'canvas';
      } else if (this.ref instanceof HTMLAudioElement) {
        return 'audio';
      } else if (this.ref.nodeName === 'REPA-SHADER') {
        return 'shader';
      }
    } else if (this._content) {
      return 'raw';
    }

    return null;
  }

  get t3d() {
    return this.hasAttribute('t3d');
  }

  get flipY() {
    return !this.t3d && this.type !== 'raw';
  }

  simpleContent(data) {
    this._format = 'luminance';
    this._width = data[0].length;
    this._height = data.length;
    const content = new Uint8Array(this._width * this._height);

    data.forEach((row, y) => {
      content.set(row, y * this._width);
    });

    this.content = content;
  }

  set content(data) {
    this.ready = true;
    this._forceUpdate = true;

    this._content = data;
  }

  get content() {
    if (this.ref) {
      if (this.type === 'shader') {
        return this.ref.target;
      } else if (this.type === 'audio') {
        return this.audioData;
      }
      return this.ref;
    }

    return this._content;
  }

  get analyser() {
    if (this.type !== 'audio' || !this.ref || !this.ref.currentTime) {
      return this._analyser;
    }

    if (!this._analyser) {
      const audioCtx = new AudioContext();
      const analyser = audioCtx.createAnalyser();

      this._binCount = analyser.frequencyBinCount;
      this._freqData = new Uint8Array(this._binCount);
      this._timeData = new Uint8Array(this._binCount);

      const source = audioCtx.createMediaElementSource(this.ref);
      source.connect(analyser);
      analyser.connect(audioCtx.destination);

      this._analyser = analyser;
    }

    return this._analyser;
  }

  get audioData() {
    // setup
    this._format = 'luminance';
    this._filter = 'nearest';

    const analyser = this.analyser;
    if (analyser) {
      analyser.getByteFrequencyData(this._freqData);
      analyser.getByteTimeDomainData(this._timeData);

      this.simpleContent([this._freqData, this._timeData]);
    } else {
      this.simpleContent([[255, 128, 64, 32, 16, 8, 4, 2], [2, 4, 8, 16, 32, 64, 128, 255]]);
    }

    return this._content;
  }

  _guessType(src) {
    if (src.toLowerCase() === 'webcam') {
      return 'webcam';
    }

    if (isImage(src)) {
      return 'image';
    }

    if (isVideo(src)) {
      return 'video';
    }

    return null;
  }

  update() {
    this._forceUpdate = false;
    return this.content;
  }

  forceUpdate() {
    this._forceUpdate = true;
  }

  get shouldUpdate() {
    return this.ready &&
      (this._forceUpdate || (
        this.ref && (
          (this.ref instanceof HTMLVideoElement && this.ref.readyState >= this.ref.HAVE_ENOUGH_DATA) ||
          (this.ref instanceof HTMLAudioElement && this.ref.readyState >= this.ref.HAVE_ENOUGH_DATA && !this.ref.paused && !this.ref.ended && this.ref.currentTime) ||
          (this.ref instanceof HTMLCanvasElement)
        )
      )
    );
  }

  get width() {
    return +(this._width || this.getAttribute("width") || this.ref?.videoWidth || this.ref?.width || 0);
  }

  get height() {
    return +(this._height || this.getAttribute("height") || this.ref?.videoHeight || this.ref?.height || 0);
  }

  get depth() {
    return +(this._depth || this.getAttribute("depth") || this.ref?.depth || 0);
  }

  get magFilter() {
    return this._filter || this.getAttribute('mag-filter') || this.getAttribute('filter') || 'linear';
  }

  get minFilter() {
    return this._filter || this.getAttribute('min-filter') || this.getAttribute('filter') || 'linear';
  }

  get wrapS() {
    return this.getAttribute('wrap-s') || this.getAttribute('wrap') || 'clamp-to-edge';
  }

  get wrapT() {
    return this.getAttribute('wrap-t') || this.getAttribute('wrap') || 'clamp-to-edge';
  }

  get wrapR() {
    return this.getAttribute('wrap-r') || this.getAttribute('wrap') || 'clamp-to-edge';
  }

  get format() {
    return this._format || this.getAttribute('format') || 'rgba';
  }

  get internalFormat() {
    return this._internalFormat || this.getAttribute('internal-format') || this.format;
  }

  get dataType() {
    return this._dataType || this.getAttribute('data-type') || 'unsigned-byte';
  }

  get name() {
    if (!this._name) {
      let name = this.getAttribute('name');
      if (!name) {
        const textures = this.parentNode.querySelectorAll('repa-texture');
        textures.forEach((texture, i) => {
          if (texture === this) {
            name = `texture${i}`;
          }
        });
      }

      this._name = name;
    }

    return this._name;
  }
}

customElements.define("repa-texture", RepaTexture);

export default RepaTexture;
