(function () {
  // N.B.: "window.status" is an HTML-defined property, and always a
  // string, so naming things at "global"-level `status` will not have
  // the desired effect!
  assertion type online();
  assertion type present(email);
  assertion type uiTemplate(name, data) = "ui-template";
  assertion type permitted(issuer, email, permission, isDelegable);
  assertion type grant(issuer, grantor, grantee, permission, isDelegable);
  assertion type permissionRequest(issuer, grantee, permission) = "permission-request";

  message type createResource(description) = "create-resource";
  message type updateResource(description) = "update-resource";
  message type deleteResource(description) = "delete-resource";

  assertion type pFollow(email) = "p:follow";
  // assertion type pInvite(email) = "p:invite";
  // assertion type pSeePresence(email) = "p:see-presence";

  var brokerConnected = Syndicate.Broker.brokerConnected;
  var brokerConnection = Syndicate.Broker.brokerConnection;
  var toBroker = Syndicate.Broker.toBroker;
  var fromBroker = Syndicate.Broker.fromBroker;
  var forceBrokerDisconnect = Syndicate.Broker.forceBrokerDisconnect;

  ///////////////////////////////////////////////////////////////////////////

  function compute_broker_url() {
    var u = new URL(document.location);
    u.protocol = (u.protocol === 'http:') ? 'ws:' : 'wss:';
    u.pathname = '/broker';
    u.hash = '';
    return u.toString();
  }

  var sessionInfo = {}; // filled in by 'load' event handler
  var brokerUrl = compute_broker_url();

  function outbound(x) {
    return toBroker(brokerUrl, x);
  }

  function inbound(x) {
    return fromBroker(brokerUrl, x);
  }

  ///////////////////////////////////////////////////////////////////////////

  window.addEventListener('load', function () {
    if (document.body.id === 'webchat-main') {
      $('head meta').each(function (_i, tag) {
        var itemprop = tag.attributes.itemprop;
        var prefix = 'webchat-session-';
        if (itemprop && itemprop.value.startsWith(prefix)) {
          var key = itemprop.value.substring(prefix.length);
          var value = tag.attributes.content.value;
          sessionInfo[key] = value;
        }
      });
      webchat_main();
    }
  });

  function webchat_main() {
    ground dataspace G {
      Syndicate.UI.spawnUIDriver({
        defaultLocationHash: '/conversations'
      });
      Syndicate.WakeDetector.spawnWakeDetector();
      Syndicate.Broker.spawnBrokerClientDriver();
      spawnInputChangeMonitor();

      actor {
        this.ui = new Syndicate.UI.Anchor();
        field this.connectedTo = null;

        assert brokerConnection(brokerUrl);

        on asserted brokerConnected($url) { this.connectedTo = url; }
        on retracted brokerConnected(_) { this.connectedTo = null; }

        var mainpage_c = this.ui.context('mainpage');
        during inbound(uiTemplate("mainpage.html", $mainpage)) {
          assert mainpage_c.html('div#main-div', mainpage);
        }

        during mainpage_c.fragmentVersion($mainpageVersion) {
          // We track mainpageVersion so that changes to mainpage.html force re-creation
          // of nested widgetry. If we didn't include mainpageVersion in each subwidget's
          // context, then so long as the subwidget's content itself remained unchanged,
          // the user would see the subwidget disappear when mainpage.html changed.
          on start { console.log('mainpage up', mainpageVersion); }
          on stop { console.log('mainpage down', mainpageVersion); }

          during inputValue('#invisible', false) {
            assert outbound(online());
          }

          on asserted Syndicate.UI.locationHash($hash) {
            var tab = hash.substr(1);
            console.log("Switching tab to", tab);
            $('#main-tabs-bodies > div').hide();
            $('#main-tabs-tabs a.nav-link').removeClass('active');
            $('#main-tab-body-' + tab).show();
            $('#main-tab-tab-' + tab).addClass('active');
          }

          during inbound(uiTemplate("present-entry.html", $presentEntry)) {
            during inbound(present($who)) {
              var c = this.ui.context(mainpageVersion, 'present', who);
              assert c.html('#present-entries', Mustache.render(
                presentEntry,
                {
                  email: who,
                  avatar: 'https://www.gravatar.com/avatar/' + md5(who.trim().toLowerCase()) + '?s=48&d=retro'
                }));
            }
          }

          during inputValue('#add-contact-email', $contact) {
            on message mainpage_c.event('#add-contact', 'click', _) {
              :: outbound(createResource(permissionRequest(contact,
                                                           sessionInfo.email,
                                                           pFollow(contact))));
              // :: outbound(createResource(permissionRequest(contact,
              //                                              sessionInfo.email,
              //                                              pInvite(contact))));
              // :: outbound(createResource(permissionRequest(contact,
              //                                              sessionInfo.email,
              //                                              pSeePresence(contact))));
              $('#add-contact-email').val('');
            }
          }

          during inbound(uiTemplate("permission-entry.html", $entry)) {
            during inbound(permitted($i, $e, $p, $d)) {
              if (i !== sessionInfo.email) {
                var c = this.ui.context(mainpageVersion, 'permitted', i, e, p, d);
                assert c.html('#permissions', Mustache.render(entry,
                                                              {issuer: i,
                                                               email: e,
                                                               permission: JSON.stringify(p),
                                                               isDelegable: d,
                                                               isRelinquishable: i !== e}));
                on message c.event('.relinquish', 'click', _) {
                  :: outbound(deleteResource(permitted(i, e, p, d)));
                }
              }
            }
          }

          during inbound(uiTemplate("grant-entry.html", $entry)) {
            during inbound(grant($i, sessionInfo.email, $ge, $p, $d)) {
              var c = this.ui.context(mainpageVersion, 'granted', i, ge, p, d);
              assert c.html('#grants', Mustache.render(entry, {issuer: i,
                                                               grantee: ge,
                                                               permission: JSON.stringify(p),
                                                               isDelegable: d}));
              on message c.event('.revoke', 'click', _) {
                :: outbound(deleteResource(grant(i, sessionInfo.email, ge, p, d)));
              }
            }
          }

          during inbound(uiTemplate("my-permission-request.html", $entry)) {
            during inbound(permissionRequest($issuer, sessionInfo.email, $permission)) {
              var c = this.ui.context(mainpageVersion, 'my-permission-request', issuer, permission);
              assert c.html('#my-permission-requests',
                            Mustache.render(entry, {issuer: issuer,
                                                    permission: JSON.stringify(permission)}));
              on message c.event('.cancel', 'click', _) {
                :: outbound(deleteResource(permissionRequest(issuer, sessionInfo.email, permission)));
              }
            }
          }

          during inbound(uiTemplate("others-permission-request.html", $entry)) {
            field this.requestCount = 0;

            assert mainpage_c.context(mainpageVersion, 'requestCount')
              .html('#request-count', this.requestCount);
            assert Syndicate.UI.uiAttribute('.request-count-sensitive',
                                            'class',
                                            'count' + this.requestCount);

            during inbound(permissionRequest($issuer, $grantee, $permission)) {
              if (grantee !== sessionInfo.email) {
                on start { this.requestCount++; }
                on stop { this.requestCount--; }

                var c = this.ui.context(mainpageVersion, 'others-permission-request', issuer, grantee, permission);
                assert c.html('#others-permission-requests',
                              Mustache.render(entry, {issuer: issuer,
                                                      grantee: grantee,
                                                      permission: JSON.stringify(permission)}));
                on message c.event('.grant', 'click', _) {
                  :: outbound(createResource(grant(issuer,
                                                   sessionInfo.email,
                                                   grantee,
                                                   permission,
                                                   false)));
                }
                on message c.event('.deny', 'click', _) {
                  :: outbound(deleteResource(permissionRequest(issuer, grantee, permission)));
                }
              }
            }
          }
        }
      }
    }
  }
})();

///////////////////////////////////////////////////////////////////////////
// Input control value monitoring

assertion type inputValue(selector, value);

function spawnInputChangeMonitor() {
  function valOf(e) {
    return e.type === 'checkbox' ? e.checked : e.value;
  }

  actor {
    during Syndicate.observe(inputValue($selector, _)) actor {
      field this.value = valOf($(selector)[0]);
      assert inputValue(selector, this.value);
      on message Syndicate.UI.globalEvent(selector, 'change', $e) {
        this.value = valOf(e.target);
      }
    }
  }
}
