(function(){
// this orchestrates multiplayer events from the scene graph

window.network = (opts) => new Proxy({

  connected: false,
  pos: '',
  posName: '',
  meetinglink: "",
  peers: {},
  plugin: {},
  opts,

  init(){
    document.addEventListener('network.disconnect', () => this.connected = false )
    document.addEventListener('network.connected',  () => this.connected = true  )
    setTimeout( () => window.frontend.emit('network.init'), 100 )
    return this
  },

  connect(opts){
    window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
  },

  add(peerid,data){
    data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
    this.peers[peerid] = data 
    window.frontend.emit(`network.peer.add`,{peer})
  },

  remove(peerid,data){
    delete this.peers[peerid]
    window.frontend.emit(`network.peer.remove`,{peer})
  },

  send(opts){
    window.frontend.emit('network.send',opts)
  },

  receive(opts){

  },

  getMeetingFromUrl(url){
    let hash = url.replace(/.*#/,'')
    let parts = hash.split("&")
    let meeting = ''
    parts.map( (p) => {
      if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
    })
    return meeting
  },

  randomRoom(){
    var names = []
    let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
    for ( var i in window             ) add(i)
    for ( var i in Object.prototype   ) add(i)
    for ( var i in Function.prototype ) add(i)
    for ( var i in Array.prototype    ) add(i)
    for ( var i in String.prototype   ) add(i) 
    var a = names[Math.floor(Math.random() * names.length)];
    var b = names[Math.floor(Math.random() * names.length)];
    return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
  }

},
{ 
  // auto-trigger events on changes 
  get(data,k,receiver){ return data[k] },
  set(data,k,v){
    let from   = data[k]
    data[k] = v
  }
})
      
document.addEventListener('frontend:ready', (e) => {
  window.network = network(e.detail).init()
  document.dispatchEvent( new CustomEvent("network:ready", e ) )
})
connectionsComponent = {

  html: `
   <div id="connections">
      <i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
      <br>
      <div class="tab-frame">
        <input type="radio" name="tab" id="login" checked>
        <label for="login">login</label>

        <input type="radio" name="tab" id="io">
        <label for="io">devices</label>
       
        <input type="radio" name="tab" id="networks">
        <label for="networks">advanced</label>
          
        <div class="tab">
          <div id="settings"></div>
          <table>
            <tr>
              <td></td>
              <td>
                <button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
              </td>
            </tr>
        </table>
        </div>

        <div class="tab">
          <div id="devices">
            <a class="badge ruler">Webcam and/or Audio</a>
            <table>
              <tr>
                <td>Video</td>
                <td>
                  <select id="videoInput"></select> 
                </td>
              </tr>
              <tr>
                <td>Mic</td>
                <td>
                  <select id="audioInput"></select> 
                </td>
              </tr>
              <tr style="display:none"> <!-- not used (for now) -->
                <td>Audio</td>
                <td>
                  <select id="audioOutput"></select> 
                </td>
              </tr>
            </table>
          </div>
        </div>

        <div class="tab">
          <div id="networking">
            Networking a la carte:<br>
            <table>
              <tr>
                <td>Webcam</td>
                <td>
                  <select id="webcam"></select>
                </td>
              </tr>
              <tr>
                <td>Chat</td>
                <td>
                  <select id="chatnetwork"></select>
                </td>
              </tr>
              <tr>
                <td>World sync</td>
                <td>
                  <select id="scene"></select>
                </td>
              </tr>
            </table>
          </div>
        </div>
      </div>
    </div>
  `,

  init: (el) => new Proxy({

    visible: true,

    webcam:       [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
    chatnetwork:  [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
    scene:        [{profile:{name:"No thanks"},config: () => document.createElement('div')}],

    selectedWebcam:     '',
    selectedChatnetwork:'',
    selectedScene:      '',

    $webcam:       $webcam      = el.querySelector("#webcam"),
    $chatnetwork:  $chatnetwork = el.querySelector("#chatnetwork"),
    $scene:        $scene       = el.querySelector("#scene"),
    $settings:     $settings    = el.querySelector("#settings"),
    $devices:      $devices     = el.querySelector("#devices"),
    $connect:      $connect     = el.querySelector("#connect"),
    $networking:   $networking  = el.querySelector("#networking"),
 
    $audioInput:   el.querySelector('select#audioInput'),
    $audioOutput:  el.querySelector('select#audioOutput'),
    $videoInput:   el.querySelector('select#videoInput'),
    
    install(opts){
      this.opts  = opts;
      (['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
      this.reactToNetwork()
      $menu.buttons = ([
        `<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>`
      ]).concat($menu.buttons)

      if( document.location.href.match(/meet=/) ) this.show()

      setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
    },

    toggle(){
      $chat.visible = !$chat.visible 
    },

    change(id,e){
      if( id.match(/^(webcam|chatnetwork|scene)$/) ){
        this.renderSettings() // trigger this when 'change' event fires on children dom elements
      }
    },

    show(opts){
      opts = opts || {}
      if( opts.hide ){
        if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
        if( !opts.showChat ) $chat.visible = false
      }else{
        $chat.visible = true
        this.visible  = true
        // hide networking settings if entering thru meetinglink
        $networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
        if( !network.connected ){
            document.querySelector('body > .xrf').appendChild(el)
            $chat.send({message:"", el, class:['ui']})
            if( !network.meetinglink ){ // set default
              $webcam.value      = opts.webcam      || 'Peer2Peer'
              $chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
              $scene.value       = opts.scene       || 'Peer2Peer'
            }
            this.renderSettings()
        }else{
          $chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
        }
      }
    },

    update(){
      this.selectedWebcam      = $webcam.value
      this.selectedChatnetwork = $chatnetwork.value
      this.selectedScene       = $scene.value
    },

    forSelectedPluginsDo(cb){
      // this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
      let plugins = {}
      let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
      this.webcam.find( select(this.selectedWebcam) )
      this.chatnetwork.find( select(this.selectedChatnetwork) )
      this.scene.find( select(this.selectedScene) )
      for( let i in plugins ){
        try{ cb(plugins[i]) }catch(e){ console.error(e) }
      }
    },

    renderSettings(){
      let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
      this.update()
      $settings.innerHTML = ''
      this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
      this.renderInputs()
    },

    renderInputs(){
      if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
        return this.$devices.style.display = 'none' 
      }else this.$devices.style.display = ''

      navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
      })
      .then( () => {

        const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];

        const gotDevices = (deviceInfos) => {
          // Handles being called several times to update labels. Preserve values.
          const values = selectors.map(select => select.value);
          selectors.forEach(select => {
            while (select.firstChild) {
              select.removeChild(select.firstChild);
            }
          });
          for (let i = 0; i !== deviceInfos.length; ++i) {
            const deviceInfo = deviceInfos[i];
            const option = document.createElement('option');
            option.value = deviceInfo.deviceId;
            if (deviceInfo.kind === 'audioinput') {
              option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
              this.$audioInput.appendChild(option);
            } else if (deviceInfo.kind === 'audiooutput') {
              option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
              this.$audioOutput.appendChild(option);
            } else if (deviceInfo.kind === 'videoinput') {
              option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
              this.$videoInput.appendChild(option);
            } else {
              console.log('Some other kind of source/device: ', deviceInfo);
            }
          }
          // hide if there's nothing to choose
          let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
          this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'

          selectors.forEach((select, selectorIndex) => {
            if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
              select.value = values[selectorIndex];
            }
          });
        }
        // after getUserMedia we can enumerate
        navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
      })
    },

    reactToNetwork(){ // *TODO* move to network?

      document.addEventListener('network.connect',    () => {
        this.show({hide:true, showChat: true})
      })
      document.addEventListener('network.disconnect',    () => {
        this.connected = false 
      })

    }

  },{

    get(data,k,v){ return data[k] },
    set(data,k,v){ 
      data[k] = v 
      switch( k ){
        case "visible":             el.style.display = v ? '' : 'none'; 
                                    if( !v && el.parentNode && el.parentNode.parentNode ) el.parentNode.parentNode.remove() 
                                    break;
        case "webcam":              $webcam.innerHTML       = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
        case "chatnetwork":         $chatnetwork.innerHTML  = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
        case "scene":               $scene.innerHTML        = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
        case "selectedScene":       $scene.value       = v; data.renderSettings(); break;
        case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
        case "selectedWebcam":      {
                                        $webcam.value      = v; 
                                        data.renderSettings(); 
                                        $devices.style.display = v ? 'block' : 'none'
                                        break;
                                    }

      } 
    }

  })
}

// reactify component!
document.addEventListener('$chat:ready', (opts) => {
  opts = opts.detail
  document.head.innerHTML += connectionsComponent.css 
  window.$connections = document.createElement('div')
  $connections.innerHTML = connectionsComponent.html
  $connections = connectionsComponent.init($connections)
  $connections.install(opts)
})

// alpine component for displaying meetings

connectionsComponent.css = `
    <style type="text/css">
      button#connect{
        height: 43px;
        width:100%;
        margin: 0px;
      }
      #messages .msg #connections{
        position:relative;
      }
      .connecthide {
        transform:translateY(-1000px);
      }
      #close{
        display: block;
        position: relative;
        float: right;
        top: 16px;
      }
      #messages .msg.ui  div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
   </style>`
chatComponent = {

  html: `
    <div id="chat">
     <div id="videos" style="pointer-events:none"></div>
     <div id="messages" aria-live="assertive" role="log" aria-relevant="additions"></div>
     <div id="chatfooter">
       <div id="chatbar">
           <input id="chatline" type="text" placeholder="chat here"></input>
       </div>
       <button id="chatsend" class="btn" aria-label="send message">
          <i class="gg-chevron-right-o"></i>
       </button>
     </div>
    </div>
  `,

  init: (el) => new Proxy({

    scene:             null,
    visible:           true,
    messages:          [],
    oneMessagePerUser: false,

    username: '',    // configured by 'network.connected' event

    $videos:         el.querySelector("#videos"),
    $messages:       el.querySelector("#messages"),
    $chatline:       el.querySelector("#chatline"),
    $chatbar:        el.querySelector("#chatbar"),
    $chatsend:       el.querySelector("#chatsend"),
    
    install(opts){
      this.opts  = opts
      this.scene = opts.scene 
      this.$chatbar.style.display = 'none'
      el.className = "xrf"
      el.style.display = 'none' // start hidden 
      document.body.appendChild( el )
      document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
      this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
    },

    sendInput(value){
      if( value[0] == '#' ) return xrf.navigator.to(value)
      let event   = value.match(/^[!\/]/) ? "chat.command" : "network.send"
      let message = value.replace(/^[!\/]/,'')
      let raw     = {detail:{message:value, halt:false}}
      document.dispatchEvent( new CustomEvent( event, {detail: {message}} ) )
      document.dispatchEvent( new CustomEvent( "chat.input", raw ) )
      if( event == "network.send" && !raw.detail.halt ) this.send({message: value })
      this.$chatline.lastValue = value
      this.$chatline.value = ''
      if( window.innerHeight < 600 ) this.$chatline.blur()
    },

    initListeners(){
      let {$chatline} = this

      $chatline.addEventListener('click', (e) => this.inform() )

      $chatline.addEventListener('keydown', (e) => {
        if (e.key == 'Enter' ){
          this.sendInput($chatline.value)
        }
        if (e.key == 'ArrowUp' ){
          $chatline.value = $chatline.lastValue || ''
        }
      })

      document.addEventListener('network.connect', (e) => {
        this.visible = true
        this.$chatbar.style.display = '' // show
      })

      document.addEventListener('network.connected', (e) => {
        if( e.detail.username ) this.username = e.detail.username
      })

      document.addEventListener('chat.command', (e) => {
        if( String(e.detail.message).trim() == 'help' ){
          let detail = {message:`The following commands are available
          <br><br>
          <b class="badge">/help</b> shows this help screen
          `}
          document.dispatchEvent( new CustomEvent( 'chat.command.help', {detail}))
          this.send({message: detail.message})
        }
      })

      this.$chatsend.addEventListener('click', (e) => {
        this.sendInput($chatline.value)
      })

    },

    inform(){
      if( !this.inform.informed && (this.inform.informed = true) ){
        window.notify("You can now type messages in the textfield below.")
      }
    },

    toggle(){
      this.visible = !this.visible
      if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
    },

    hyphenate(str){
      return String(str).replace(/[^a-zA-Z0-9]/g,'-')
    },

    // sending messages to the #messages div 
    // every user can post maximum one msg at a time 
    // it's more like a 'status' which is more friendly 
    // for accessibility reasons 
    // for a fullfledged chat/transcript see matrix clients
    send(opts){
      let {$messages} = this
      opts = { linebreak:true, message:"", class:[], ...opts }
      if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
      opts.pos  = opts.pos || network.posName || network.pos
      let div  = document.createElement('div')
      let msg  = document.createElement('div')
      let br   = document.createElement('br')
      let nick = document.createElement('div')
      msg.className = "msg"
      let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
      if( $messages.last == html ) return
      msg.innerHTML = html 
      if( opts.el ) msg.appendChild(opts.el)
      opts.id       = Math.random()
      if( opts.class ){
        msg.classList.add.apply(msg.classList, opts.class)
        br.classList.add.apply(br.classList, opts.class)
        div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
      }
      if( !msg.className.match(/(info|guide|ui)/) && !opts.from ){
        let frag = xrf.URI.parse(document.location.hash).XRF
        opts.from = 'you'
        if( frag.pos ) opts.pos = frag.pos.string
        msg.classList.add('self')
      }
      if( opts.from ){
        nick.className = "user"
        nick.innerText = opts.from+' '
        div.appendChild(nick)
        if( opts.pos ){
          let a = document.createElement("a")
          a.href = a.innerText = `#pos=${opts.pos}`
          nick.appendChild(a)
        }
      }
      div.appendChild(msg)
      // force one message per user 
      if( this.oneMessagePerUser && opts.from ){
        div.id = this.hyphenate(opts.from)
        let oldMsg = $messages.querySelector(`#${div.id}`)
        if( oldMsg ) oldMsg.remove()
      }
      // remove after timeout
      if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
      // finally add the message on top
      $messages.appendChild(div)
      if( opts.linebreak ) div.appendChild(br)
      $messages.scrollTop = $messages.scrollHeight // scroll down
      $messages.last = msg.innerHTML
    },

    getChatLog(){
        return ([...this.$messages.querySelectorAll('.envelope')])
                .filter( (d) => !d.className.match(/(info|ui)/) ) 
                .map( (d) => d.innerHTML )
                .join('\n')
    }

  },{

    get(me,k,v){ return me[k] },
    set(me,k,v){ 
      me[k] = v    
      switch( k ){
        case "visible":         { 
                                  el.style.display = me.visible ? 'block' : 'none'
                                  if( !el.inited && (el.inited = true) ) me.initListeners()
                                  break;
                                }
      }
    }

  })
}

// reactify component!
document.addEventListener('$menu:ready', (opts) => {
  opts = opts.detail
  document.head.innerHTML += chatComponent.css 
  window.$chat = document.createElement('div')
  $chat.innerHTML = chatComponent.html
  $chat = chatComponent.init($chat)
  $chat.install(opts)
  //$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="toggle text" id="meeting" onclick="$chat.toggle()">📜 toggle text</a><br>`])
  //                  .concat($menu.buttons)
})

// alpine component for displaying meetings

chatComponent.css = `
    <style type="text/css">
     #videos{
       display:grid-auto-columns;
       grid-column-gap:5px;
       margin-bottom:15px;
       position: fixed;
       top: 0;
       left: 0;
       bottom: 0;
       right: 0;
       margin: 15px;
       z-index:1000;
     }
     #videos > video{
       border-radius:7px;
       display:inline-block;
       background:black;
       width:80px;
       height:60px;
       margin-right:5px;
       margin-bottom:5px;
       vertical-align:top;
       pointer-events:all;
     }
     #videos > video:hover{
       filter: brightness(1.8);
       cursor:pointer;
     }

     #chatbar,
     button#chatsend{
       z-index: 1500;
       position: fixed;
       bottom: 24px;
       height: 34px;
       left: 20px;
       width: 48%;
       background: white;
       padding: 0px 0px 0px 15px; 
       border-radius: 30px;
       max-width: 500px;
       box-sizing: border-box;
       box-shadow: 0px 0px 5px 5px #0002;
     }
     button#chatsend{
      line-height:0px;
      display:none;
      z-index: 1550;
      color: white;
      border: 0;
      height: 35px;
      background: var(--xrf-dark-gray);
      font-weight: bold;
      width: 20px;
      max-width: 20px;
      border-radius: 20px 0px 0px 20px;
      overflow: hidden;
     }
     #chatbar input{
       border:none;
       width:90%;
       box-sizing:border-box;
       height: 24px;
       font-size: var(--xrf-font-size-2);
       max-width:unset;
     }
     #messages{
       /*
       display: flex;
       flex-direction: column;
       width: 100%;
       max-width: 40%;
       */
       width:100%;
       box-sizing:border-box;
       align-items: flex-start;
       position: absolute;
       transition:1s;
       top: 77px;
       left: 0;
       bottom: 49px;
       padding: 20px;
       overflow:hidden;
       overflow-y: auto;
       pointer-events:none;
       transition:1s;
       z-index: 100;
     }
     body.menu #messages{
       top:50px;
     }
     #messages:hover {
       pointer-events:all;
     }
     #messages *{
       box-sizing:border-box;
/*
       pointer-events:none;
       -webkit-user-select:none;
       -moz-user-select:-moz-none;
       -ms-user-select:none;
       user-select:none;
*/
     }
     #messages .msg{
       transition:all 1s ease;
       background: #fff;
       display: inline-block;
       padding: 1px 17px;
       border-radius: 20px;
       color: #000c;
       margin-bottom: 10px;
       line-height:23px;
       line-height:33px;
       cursor:grabbing;
       border: 1px solid #0002;
     }
     #messages .msg *,
     #messages .user *{
       pointer-events:all;
       -webkit-user-select:text;
       -moz-user-select:-moz-text;
       -ms-user-select:text;
       user-select:text;
    }

     #messages .msg.self{
       border-radius: 20px;
       background:var(--xrf-dark-gray);
     }
     #messages .msg.self,
     #messages .msg.self div{
       color:#FFF;
     }
     #messages .msg.info{
       background: var(--xrf-white);
       border-radius: 20px;
       color: var(--xrf-dark-gray);
       text-align: left;
       line-height: 19px;
     }
     #messages .msg.info,
     #messages .msg.info *{
       font-size: var(--xrf-font-size-0);
     }
     #messages .msg a {
       text-decoration:underline;
       color: var(--xrf-light-xrf-secondary);
       font-weight:bold;
       transition:0.3s;
     }
     #messages .msg.info a,
     #messages a.ruler{
       color:#FFF;
     }
     #messages .msg a:hover{
        color:#000;
     }
     #messages .msg.ui, 
     #messages .msg.ui div{ 
       background: white;
       border:none;
       color: #333;
       border-radius: 20px;
       margin:0;
       padding:0px 5px 5px 5px;
     }
     #messages.guide, .guide{
      display:unset;
     }
     #messages .guide, .guide{
       display:none;
     }
     br.guide{
       display:inline-block;
     }
     #messages .msg.info a:hover,
     #messages button:hover{
       filter: brightness(1.4);
     }
     #messages .msg.multiline {
       padding: 2px 14px;
     }
     #messages button {
       text-decoration:none;
       margin: 0px 15px 10px 0px;
       background: var(--xrf-primary);
       font-family: var(--xrf-font-sans-serif);
       color: #FFF;
       border-radius: 7px;
       padding: 11px 15px;
       border: 0;
       font-weight: bold;
       box-shadow: 0px 0px 5px 5px #0002;
       pointer-events:all;
     }
     #messages,#chatbar,#chatbar *, #messages *{
     }


    #messages button.emoticon,
    #messages .btn.emoticon {
      line-height:2px;
      width: 20px;
      display: inline-block;
      padding: 0px 0px;
      margin: 0;
      vertical-align: middle;
      background: none;
      border: none;
      min-width: 31px;
      box-shadow:none;
    }

    #messages button.emoticon:hover,
    #messages .btn.emoticon:hover {
      border: 1px solid #ccc !important;
      background:#EEE;
    }

    .nomargin{
      margin:0;
    }

    .envelope{
      margin-right:15px;
      width:100%;
      max-width:40%;
    }

    .envelope,
    .envelope * {
      transition:1s;
      pointer-events:none;
    }
    .envelope a,
    .envelope button,
    .envelope input,
    .envelope textarea,
    .envelope msg,
    .envelope msg * {
      pointer-events:all;
    }

    .user{
      margin-left:13px;
      font-weight: bold;
      color: var(--xrf-dark-gray);
    }
    .user, .user *{ 
      font-size: var(--xrf-font-size-0);
    }
    .gg-chevron-right-o {
      color:#FFF;
      box-sizing: border-box;
      position: relative;
      display: block;
      transform: scale(var(--ggs,1));
      width: 22px;
      height: 22px;
      border: 2px solid;
      border-radius: 100px
    }

    .gg-chevron-right-o::after {
      color:#FFF;
      content: "";
      display: block;
      box-sizing: border-box;
      position: absolute;
      width: 6px;
      height: 6px;
      border-bottom: 2px solid;
      border-right: 2px solid;
      transform: rotate(-45deg);
      left: 5px;
      top: 6px
    }
   </style>`
window.debug = (opts) => new Proxy({
  opts,

  enabled: false,
  $console: false,


  toggle(){ this.enabled = !this.enabled },

  settings(){
    this.toggle()
  },

  init(){
  },

  setupConsole(){
    // add onscreen console
    let $console = this.$console = document.createElement('pre')
    $console.style.position = 'fixed'
    $console.style.overflow = 'auto'
    $console.style.top = $console.style.left = $console.style.bottom = $console.style.right = '0px'
    $console.style.height = '98.5vh';
    $console.style.width = '100%'
    $console.style.pointerEvents = 'none'
    $console.id = 'console'
    document.body.appendChild($console)
    const wrapper = (scope, fn, name) => {
      return function(msg) {
        $console.innerHTML += `[${name}] ${msg}<br>`;
        if( name == 'err'){
          let err = new Error()
          String(err.stack).split("\n").slice(2).map( (l) => $console.innerHTML += ` └☑ ${l}\n` )
        }
        $console.scrollTop = $console.scrollHeight;
        fn.call(scope,msg);
      };
    }
    window.console.log   = wrapper(console, console.log, "log");
    window.console.warn  = wrapper(console, console.warn, "wrn");
    window.console.error = wrapper(console, console.error, "err");
  }

},
{ 
  // auto-trigger events on changes 
  get(data,k,receiver){ return data[k] },
  set(data,k,v){ 
    data[k] = v 
    switch( k ){
      case "enabled": {
                        if( !data.$console ) data.setupConsole()
                        $('#debug.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
                        data.$console.style.display = v ? 'block' : 'none'
                        data.enabled = v
                      }
    }
  }
})

document.addEventListener('$menu:ready', (e) => {
  try{
    debug = debug(e.detail) 
    debug.init()
    document.dispatchEvent( new CustomEvent("debug:ready", e ) )
    $menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all debug features" id="debug" onclick="debug.enabled = !debug.enabled"><i class="gg-debug"></i>debug</a><br>`])
  }catch(e){console.error(e)}
})

document.querySelector('head').innerHTML += `
  <style type="text/css"> 
    .gg-debug {
       box-sizing: border-box;
       position: relative;
       display: block;
       transform: scale(var(--ggs,1));
       width: 12px;
       height: 18px;
       border: 2px solid;
       border-radius: 22px;
       display: inline-block;
       transform: translate(0px,4px);
       margin-right: 21px;
    }
    .gg-debug::after,
    .gg-debug::before {
       content: "";
       display: block;
       box-sizing: border-box;
       position: absolute
    }
    .gg-debug::before {
       width: 8px;
       height: 4px;
       border: 2px solid;
       top: -4px;
       border-bottom-left-radius: 10px;
       border-bottom-right-radius: 10px;
       border-top: 0
    }
    .gg-debug::after {
       background: currentColor;
       width: 4px;
       height: 2px;
       border-radius: 5px;
       top: 4px;
       left: 2px;
       box-shadow:
          0 4px 0,
          -6px -2px 0,
          -6px 2px 0,
          -6px 6px 0,
          6px -2px 0,
          6px 2px 0,
          6px 6px 0
    }
  </style>
`

}).apply({})