window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame    ||
    window.oRequestAnimationFrame      ||
    window.msRequestAnimationFrame     ||
    function(callback){
      window.setTimeout(callback, 1000 / 8);
    };
})();

(function() {
  'use strict';

  // Initial setup
  function $(x) {return document.getElementById(x);}

  let $container = $("banner-textarea");
  let $message = $("banner-msg");
  let $animate = $("banner-msg-animate");
  let $paragraph = null;

  const asciiEscMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '`': '&#x60;'
  };

  function needsAsciiEsc(str) {
    for (let i of str) {
      if (asciiEscMap[i]) { return true; }
    }
    return false;
  }
  function isAsciiString(str) {
    for (let i of str) {
      if (i.codePointAt(0) > 127) { return false; }
    }
    return true;
  }
  function maybeEscpDec(ch) {
    const cp = ch.codePointAt(0);
    return cp > 127 ? '&#' + cp + ';' : ch;
  }
  function escpAscii(str) {
    return [...str].map(x => asciiEscMap[x] || x).join("");
  }

  function escp(text) {
    if (needsAsciiEsc(text)) {
      text = escpAscii(text);
    }
    return isAsciiString(text) ? text : [...text].map(maybeEscpDec).join("");
  }

  // Messages setup

  /* MESSAGES be like:
   * [ {"delay": 0,    "text": "message 1"},
   *   {"delay": 1200, "text": "message 2"},
   *   {"delay": 2200, "text": "message 4"},
   *   {"delay": 3600, "text": "message 5"},
   *   {"delay": 5200, "text": "message 6"} ]
   */
  const MESSAGES = JSON.parse($animate.getAttribute('data-msg'));

  function scramble($element, text, options) {
    const defaults = {
      probability: 0.25,
      glitches: "-%¥¶!\"❏_△§*¢ 	¿",
      blank: "",
      duration: text.length * 80,
      delay: 0.0
    };
    const settings = {...defaults, ...options};

    function shuffle() {return (Math.random() < 0.5) ? 1 : -1;}
    function wrap(text, classes) {return "<span class=\"" + classes + "\">" + text + "</span>";}

    const glitchCharacters = [...settings.glitches].map(escp);
    const glitchLength = glitchCharacters.length;
    const glitchProbability = settings.probability;
    const glitches = glitchCharacters.map(x => wrap(x, "pop"));

    const ghostCharacters = [...$element.innerText].map(escp);
    const ghostLength = ghostCharacters.length;
    const ghosts = ghostCharacters.map(x => wrap(x, "ghost"));
    const textCharacters = [...text].map(escp);
    const textLength = textCharacters.length;
    const order = Array.from(Array(textLength).keys()).sort(shuffle);

    let output = [];
    for (let i = 0; i < textLength; ++i) {
      const index = Math.floor(Math.random() * (glitchLength - 1));
      const glitchCharacter = glitches[index];
      const ghostCharacter = ghosts[i] || settings.blank;
      const character = Math.random() < glitchProbability ? glitchCharacter : ghostCharacter;
      output.push(character);
    }

    // Animation
    const duration = settings.duration;
    const ease = settings.ease;

    let start = null;
    const TRESH = 1000 / 8;  // 8FPS, more than enough

    function easeInOutQuad(t) {
      if (t < 0.5)
        return 2 * t * t;

      return Math.min((4 - 2 * t) * t - 1, 1);
    }
    function refresh() {
      const now = Date.now();
      if (!start) { start = now; }

      const elapsed = now - start;
      if (elapsed < TRESH) {
        window.requestAnimFrame(refresh);
        return;
      }
      if (elapsed > duration) {
        $element.innerHTML = text;
        return;
      }

      const interp = easeInOutQuad(elapsed / duration);
      const progress = Math.floor(interp * (textLength - 1));
      for (let i = 0; i < progress; ++i) {
        const index = order[i];
        output[index] = textCharacters[index];
      }

      $element.innerHTML = output.join('');
      window.requestAnimFrame(refresh);
    }

    setTimeout(refresh, settings.delay);
  }

  function animate() {
    for (let i = 0; i < MESSAGES.length; ++i) {
      let data = MESSAGES[i];
      let element = $paragraph.item(i);

      element.innerText = '';
      let options = { delay: data.delay };

      scramble(element, data.text, options);
    }
  }

  function initialize() {
    for (let _ in MESSAGES) {
      let elem = document.createElement("p");
      elem.className = "banner-msg-line";
      $message.appendChild(elem);
    }
    $paragraph = $container.getElementsByTagName("p");
    animate();
  }

  initialize();
}).call(this);
