text: /**
   * @provides javelin-behavior-pholio-mock-view
   * @requires javelin-behavior
   *           javelin-util
   *           javelin-stratcom
   *           javelin-dom
   *           javelin-vector
   *           javelin-magical-init
   *           javelin-request
   *           javelin-history
   *           javelin-workflow
   *           javelin-mask
   *           javelin-behavior-device
   *           phabricator-keyboard-shortcut
   */
  JX.behavior('pholio-mock-view', function(config) {
    var is_dragging = false;
  
    var drag_begin;
    var drag_end;
    var panel = JX.$(config.panelID);
    var viewport = JX.$(config.viewportID);
  
    var selection_reticle;
    var active_image;
  
    var inline_comments = {};
  
  
  /* -(  Stage  )-------------------------------------------------------------- */
  
  
    var stage = (function() {
      var loading = false;
      var stageElement = JX.$(config.panelID);
      var viewElement = JX.$(config.viewportID);
      var reticles = [];
  
      function begin_load() {
        if (loading) {
          return;
        }
        loading = true;
        clear_stage();
        draw_loading();
      }
  
      function end_load() {
        if (!loading) {
          return;
        }
        loading = false;
        draw_loading();
      }
  
      function draw_loading() {
        JX.DOM.alterClass(stageElement, 'pholio-image-loading', loading);
      }
  
      function add_reticle(reticle, id) {
        mark_ref(reticle, id);
        reticles.push(reticle);
        viewElement.appendChild(reticle);
      }
  
      function clear_stage() {
        var ii;
        for (ii = 0; ii < reticles.length; ii++) {
          JX.DOM.remove(reticles[ii]);
        }
        reticles = [];
      }
  
      function mark_ref(node, id) {
        JX.Stratcom.addSigil(node, 'pholio-inline-ref');
        JX.Stratcom.addData(node, {inlineID: id});
      }
  
      return {
        beginLoad: begin_load,
        endLoad: end_load,
        addReticle: add_reticle,
        clearStage: clear_stage
      };
    })();
  
    JX.enableDispatch(document.body, 'mouseenter');
    JX.enableDispatch(document.body, 'mouseleave');
  
    JX.Stratcom.listen(
      ['mouseenter', 'mouseover'],
      'mock-panel',
      function(e) {
        JX.DOM.alterClass(e.getNode('mock-panel'), 'mock-has-cursor', true);
      });
  
    JX.Stratcom.listen('mouseleave', 'mock-panel', function(e) {
      var node = e.getNode('mock-panel');
      if (e.getTarget() == node) {
        JX.DOM.alterClass(node, 'mock-has-cursor', false);
      }
    });
  
    function get_image_index(id) {
      for (var ii = 0; ii < config.images.length; ii++) {
        if (config.images[ii].id == id) {
          return ii;
        }
      }
      return null;
    }
  
    function get_image_navindex(id) {
      for (var ii = 0; ii < config.navsequence.length; ii++) {
        if (config.navsequence[ii] == id) {
          return ii;
        }
      }
      return null;
    }
  
    function get_image(id) {
      var idx = get_image_index(id);
      if (idx === null) {
        return idx;
      }
      return config.images[idx];
    }
  
    function onload_image(id) {
      if (active_image.id != id) {
        // The user has clicked another image before this one loaded, so just
        // bail.
        return;
      }
  
      active_image.tag = this;
      redraw_image();
    }
  
    function switch_image(delta) {
      if (!active_image) {
        return;
      }
      var idx = get_image_navindex(active_image.id);
      if (idx === null) {
        return;
      }
      idx = (idx + delta + config.navsequence.length) % config.navsequence.length;
      select_image(config.navsequence[idx]);
    }
  
    function redraw_image() {
      var new_y;
  
      // If we don't have an image yet, just scale the stage relative to the
      // entire viewport height so the jump isn't too jumpy when the image loads.
      if (!active_image || !active_image.tag) {
        new_y = (JX.Vector.getViewport().y * 0.80);
        new_y = Math.max(320, new_y);
        panel.style.height = new_y + 'px';
  
        return;
      }
  
      var tag = active_image.tag;
  
      // If the image is too wide for the viewport, scale it down so it fits.
      // If it is too tall, just let the viewport scroll.
      var w = JX.Vector.getDim(panel);
  
      // Leave 24px margins on either side of the image.
      w.x -= 48;
  
      var scale = 1;
      if (w.x < tag.naturalWidth) {
        scale = Math.min(scale, w.x / tag.naturalWidth);
      }
  
      if (scale < 1) {
        tag.width = Math.floor(scale * tag.naturalWidth);
        tag.height = Math.floor(scale * tag.naturalHeight);
      } else {
        tag.width = tag.naturalWidth;
        tag.height = tag.naturalHeight;
      }
  
      // Scale the viewport's vertical size to the image's adjusted size.
      new_y = Math.max(320, tag.height + 48);
      panel.style.height = new_y + 'px';
  
      viewport.style.top = Math.floor((new_y - tag.height) / 2) + 'px';
  
      stage.endLoad();
  
      JX.DOM.setContent(viewport, tag);
  
      redraw_inlines(active_image.id);
    }
  
    function select_image(image_id) {
      active_image = get_image(image_id);
      active_image.tag = null;
  
      stage.beginLoad();
  
      var img = JX.$N('img', {className: 'pholio-mock-image'});
      img.onload = JX.bind(img, onload_image, active_image.id);
      img.src = active_image.stageURI;
  
      var thumbs = JX.DOM.scry(
        JX.$('pholio-mock-thumb-grid'),
        'a',
        'mock-thumbnail');
  
      for(var k in thumbs) {
        var thumb_meta = JX.Stratcom.getData(thumbs[k]);
  
        JX.DOM.alterClass(
          thumbs[k],
          'pholio-mock-thumb-grid-current',
          (active_image.id == thumb_meta.imageID));
      }
  
      load_inline_comments();
      if (image_id != config.selectedID) {
        JX.History.replace(active_image.pageURI);
      }
    }
  
    JX.Stratcom.listen(
      'click',
      'mock-thumbnail',
      function(e) {
        if (!e.isNormalMouseEvent()) {
          return;
        }
        e.kill();
        select_image(e.getNodeData('mock-thumbnail').imageID);
      });
  
    select_image(config.selectedID);
  
    JX.Stratcom.listen('mousedown', 'mock-viewport', function(e) {
      if (!e.isNormalMouseEvent()) {
        return;
      }
  
      if (JX.Device.getDevice() != 'desktop') {
        return;
      }
  
      if (JX.Stratcom.pass()) {
        return;
      }
  
      if (is_dragging) {
        return;
      }
  
      e.kill();
  
      if (!active_image.isImage) {
        // If this is a PDF or something like that, we eat the event but we
        // don't let users add inlines to the thumbnail.
        return;
      }
  
      is_dragging = true;
      drag_begin = get_image_xy(JX.$V(e));
      drag_end = drag_begin;
  
      redraw_selection();
    });
  
  
    JX.enableDispatch(document.body, 'mousemove');
    JX.Stratcom.listen('mousemove', null, function(e) {
      if (!is_dragging) {
        return;
      }
      drag_end = get_image_xy(JX.$V(e));
      redraw_selection();
    });
  
    JX.Stratcom.listen(
      'mousedown',
      'pholio-inline-ref',
      function(e) {
        e.kill();
  
        var id = e.getNodeData('pholio-inline-ref').inlineID;
  
        var active_id = active_image.id;
        var handler = function(r) {
          var inlines = inline_comments[active_id];
  
          for (var ii = 0; ii < inlines.length; ii++) {
            if (inlines[ii].id == id) {
              if (r.id) {
                inlines[ii] = r;
              } else {
                inlines.splice(ii, 1);
              }
              break;
            }
          }
  
          redraw_inlines(active_id);
          JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
        };
  
        new JX.Workflow('/pholio/inline/' + id + '/')
          .setHandler(handler)
          .start();
      });
  
    JX.Stratcom.listen(
      'mouseup',
      null,
      function(e) {
        if (!is_dragging) {
          return;
        }
  
        is_dragging = false;
        if (!config.loggedIn) {
          new JX.Workflow(config.logInLink).start();
          return;
        }
  
        drag_end = get_image_xy(JX.$V(e));
  
        resize_selection(16);
  
        var data = {
          mockID: config.mockID,
          imageID: active_image.id,
          startX: Math.min(drag_begin.x, drag_end.x),
          startY: Math.min(drag_begin.y, drag_end.y),
          endX: Math.max(drag_begin.x, drag_end.x),
          endY: Math.max(drag_begin.y, drag_end.y)
        };
  
        var handler = function(r) {
          if (!inline_comments[active_image.id]) {
            inline_comments[active_image.id] = [];
          }
          inline_comments[active_image.id].push(r);
  
          redraw_inlines(active_image.id);
          JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
        };
  
        clear_selection();
  
        new JX.Workflow('/pholio/inline/', data)
          .setHandler(handler)
          .start();
      });
  
    function resize_selection(min_size) {
      var start = {
        x: Math.min(drag_begin.x, drag_end.x),
        y: Math.min(drag_begin.y, drag_end.y)
      };
      var end = {
        x: Math.max(drag_begin.x, drag_end.x),
        y: Math.max(drag_begin.y, drag_end.y)
      };
  
      var width = end.x - start.x;
      var height = end.y - start.y;
      var addon;
  
      if (width < min_size) {
        addon = (min_size-width)/2;
  
        start.x = Math.max(0, start.x - addon);
        end.x = Math.min(active_image.tag.naturalWidth, end.x + addon);
  
        if (start.x === 0) {
          end.x = Math.min(min_size, active_image.tag.naturalWidth);
        } else if (end.x == active_image.tag.naturalWidth) {
          start.x = Math.max(0, active_image.tag.naturalWidth - min_size);
        }
      }
  
      if (height < min_size) {
        addon = (min_size-height)/2;
  
        start.y = Math.max(0, start.y - addon);
        end.y = Math.min(active_image.tag.naturalHeight, end.y + addon);
  
        if (start.y === 0) {
          end.y = Math.min(min_size, active_image.tag.naturalHeight);
        } else if (end.y == active_image.tag.naturalHeight) {
          start.y = Math.max(0, active_image.tag.naturalHeight - min_size);
        }
      }
  
      drag_begin = start;
      drag_end = end;
      redraw_selection();
    }
  
    function render_image_header(image) {
      // Render image dimensions and visible size. If we have this infomation
      // from the server we can display some of it immediately; otherwise, we need
      // to wait for the image to load so we can read dimension information from
      // it.
  
      var image_x = image.width;
      var image_y = image.height;
      var display_x = null;
      if (image.tag) {
        image_x = image.tag.naturalWidth;
        image_y = image.tag.naturalHeight;
        display_x = image.tag.width;
      }
  
      var visible = [];
      if (image_x) {
        if (display_x) {
          var area = Math.round(100 * (display_x / image_x));
          visible.push(
            JX.$N(
              'span',
              {className: 'pholio-visible-size'},
              [area, '%']));
          visible.push(' ');
        }
        visible.push(['(', image_x, ' \u00d7 ', image_y, ')']);
      }
  
      return visible;
    }
  
    function redraw_inlines(id) {
      if (!active_image) {
        return;
      }
  
      if (active_image.id != id) {
        return;
      }
  
      stage.clearStage();
      var comment_holder = JX.$('mock-image-description');
      JX.DOM.setContent(comment_holder, render_image_info(active_image));
  
      var image_header = JX.$('mock-image-header');
      JX.DOM.setContent(image_header, render_image_header(active_image));
  
      var inlines = inline_comments[active_image.id];
      if (!inlines || !inlines.length) {
        return;
      }
  
      for (var ii = 0; ii < inlines.length; ii++) {
        var inline = inlines[ii];
  
        if (!active_image.tag) {
          // The image itself hasn't loaded yet, so we can't draw the inline
          // reticles.
          continue;
        }
  
        var classes = [];
        if (!inline.transactionPHID) {
          classes.push('pholio-mock-reticle-draft');
        } else {
          classes.push('pholio-mock-reticle-final');
        }
  
        var inline_selection = render_reticle(classes,
          'pholio-mock-comment-icon phui-font-fa fa-comment');
        stage.addReticle(inline_selection, inline.id);
        position_inline_rectangle(inline, inline_selection);
      }
    }
  
    function position_inline_rectangle(inline, rect) {
      var scale = get_image_scale();
  
      JX.$V(scale * inline.x, scale * inline.y).setPos(rect);
      JX.$V(scale * inline.width, scale * inline.height).setDim(rect);
    }
  
    function get_image_xy(p) {
      var img = active_image.tag;
      var imgp = JX.$V(img);
  
      var scale = 1 / get_image_scale();
  
      var x = scale * Math.max(0, Math.min(p.x - imgp.x, img.width));
      var y = scale * Math.max(0, Math.min(p.y - imgp.y, img.height));
  
      return {
        x: x,
        y: y
      };
    }
  
    function get_image_scale() {
      var img = active_image.tag;
      return Math.min(
        img.width / img.naturalWidth,
        img.height / img.naturalHeight);
    }
  
    function redraw_selection() {
      var classes = ['pholio-mock-reticle-selection'];
      selection_reticle = selection_reticle || render_reticle(classes, '');
  
      var p = JX.$V(
        Math.min(drag_begin.x, drag_end.x),
        Math.min(drag_begin.y, drag_end.y));
  
      var d = JX.$V(
        Math.max(drag_begin.x, drag_end.x) - p.x,
        Math.max(drag_begin.y, drag_end.y) - p.y);
  
      var scale = get_image_scale();
  
      p.x *= scale;
      p.y *= scale;
      d.x *= scale;
      d.y *= scale;
  
      viewport.appendChild(selection_reticle);
      p.setPos(selection_reticle);
      d.setDim(selection_reticle);
    }
  
    function clear_selection() {
      selection_reticle && JX.DOM.remove(selection_reticle);
      selection_reticle = null;
    }
  
    function load_inline_comments() {
      var id = active_image.id;
      var inline_comments_uri = '/pholio/inline/list/' + id + '/';
  
      new JX.Request(inline_comments_uri, function(r) {
        inline_comments[id] = r;
        redraw_inlines(id);
      }).send();
    }
  
    load_inline_comments();
    if (config.loggedIn && config.commentFormID) {
      JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
    }
  
    JX.Stratcom.listen('resize', null, redraw_image);
    redraw_image();
  
  
  /* -(  Keyboard Shortcuts  )------------------------------------------------- */
  
  
    new JX.KeyboardShortcut(['j', 'right'], 'Show next image.')
      .setHandler(function() {
        switch_image(1);
      })
      .register();
  
    new JX.KeyboardShortcut(['k', 'left'], 'Show previous image.')
      .setHandler(function() {
        switch_image(-1);
      })
      .register();
  
    JX.DOM.listen(panel, 'gesture.swipe.end', null, function(e) {
      var data = e.getData();
  
      if (data.length <= (JX.Vector.getDim(panel) / 2)) {
        // If the user didn't move their finger far enough, don't switch.
        return;
      }
  
      switch_image(data.direction == 'right' ? -1 : 1);
    });
  
  /* -(  Render  )------------------------------------------------------------- */
  
  
    function render_image_info(image) {
      var info = [];
  
      var buttons = [];
  
      var classes = ['pholio-image-button'];
  
      if (image.isViewable) {
        classes.push('pholio-image-button-active');
      } else {
        classes.push('pholio-image-button-disabled');
      }
  
      buttons.push(
        JX.$N(
          'div',
          {
            className: classes.join(' ')
          },
          JX.$N(
            image.isViewable ? 'a' : 'span',
            {
              href: image.fullURI,
              target: '_blank',
              className: 'pholio-image-button-link'
            },
            JX.$H(config.fullIcon))));
  
      classes = ['pholio-image-button', 'pholio-image-button-active'];
  
      buttons.push(
        JX.$N(
          'form',
          {
            className: classes.join(' '),
            action: image.downloadURI,
            method: 'POST',
            sigil: 'download'
          },
          JX.$N(
            'button',
            {
              href: image.downloadURI,
              className: 'pholio-image-button-link'
            },
            JX.$H(config.downloadIcon))));
  
      if (image.title === '') {
        image.title = 'Untitled Masterpiece';
      }
      var title = JX.$N(
        'div',
        {className: 'pholio-image-title'},
        image.title);
      info.push(title);
  
      if (!image.isObsolete) {
        var img_len = config.currentSetSize;
        var rev = JX.$N(
          'div',
          {className: 'pholio-image-revision'},
          JX.$H('Current Revision (' + img_len + ' images)'));
        info.push(rev);
      } else {
        var prev = JX.$N(
          'div',
          {className: 'pholio-image-revision'},
          JX.$H('(Previous Revision)'));
        info.push(prev);
      }
  
      for (var ii = 0; ii < info.length; ii++) {
        info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]);
      }
      info = JX.$N('div', {className: 'pholio-image-info'}, info);
  
      if (image.descriptionMarkup === '') {
        return [buttons, info];
      } else {
        var desc = JX.$N(
          'div',
          {className: 'pholio-image-description'},
          JX.$H(image.descriptionMarkup));
        return [buttons, info, desc];
      }
    }
  
    function render_reticle(classes, inner_classes) {
      var inner = JX.$N('div', {className: inner_classes});
      var outer = JX.$N(
        'div',
        {className: ['pholio-mock-reticle'].concat(classes).join(' ')}, inner);
      return outer;
    }
  
  
  /* -(  Device Lightbox  )---------------------------------------------------- */
  
    // On devices, we show images full-size when the user taps them instead of
    // attempting to implement inlines.
  
    var lightbox = null;
  
    JX.Stratcom.listen('click', 'mock-viewport', function(e) {
      if (!e.isNormalMouseEvent()) {
        return;
      }
      if (JX.Device.getDevice() == 'desktop') {
        return;
      }
      lightbox_attach();
      e.kill();
    });
  
    JX.Stratcom.listen('click', 'pholio-device-lightbox', lightbox_detach);
    JX.Stratcom.listen('resize', null, lightbox_resize);
  
    function lightbox_attach() {
      JX.DOM.alterClass(document.body, 'lightbox-attached', true);
      JX.Mask.show('jx-dark-mask');
  
      lightbox = lightbox_render();
      var image = JX.$N('img');
      image.onload = lightbox_loaded;
      setTimeout(function() {
        image.src = active_image.stageURI;
      }, 1000);
      JX.DOM.setContent(lightbox, image);
      JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', true);
  
      lightbox_resize();
  
      document.body.appendChild(lightbox);
    }
  
    function lightbox_detach() {
      JX.DOM.remove(lightbox);
      JX.Mask.hide();
      JX.DOM.alterClass(document.body, 'lightbox-attached', false);
      lightbox = null;
    }
  
    function lightbox_resize() {
      if (!lightbox) {
        return;
      }
      JX.Vector.getScroll().setPos(lightbox);
      JX.Vector.getViewport().setDim(lightbox);
    }
  
    function lightbox_loaded() {
      JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', false);
    }
  
    function lightbox_render() {
      var el = JX.$N('div', {className: 'pholio-device-lightbox'});
      JX.Stratcom.addSigil(el, 'pholio-device-lightbox');
      return el;
    }
  
  
  /* -(  Preload  )------------------------------------------------------------ */
  
    var preload = [];
    for (var ii = 0; ii < config.images.length; ii++) {
      preload.push(config.images[ii].stageURI);
    }
  
    function preload_next() {
      next_src = preload[0];
      if (!next_src) {
        return;
      }
      preload.splice(0, 1);
  
      var img = JX.$N('img');
      img.onload = preload_next;
      img.onerror = preload_next;
      img.src = next_src;
    }
  
    preload_next();
  
  
  });
  