/*
  SpacedeckSections
  This module contains functions dealing with Space Sections UI.
*/

var SpacedeckSections = {

  data: {
    MAX_COLUMNS: 20,

    redo_stack: [],
    undo_stack: [],

    opened_dialog: "none", // which property panel to display
    color_options_picker: false,
    advanced_properties: false,

    embed_code_html: "",
    active_tool: "pointer",
    lightbox_artifact: {},
    snap_ruler_y: -1000,
    snap_ruler_x: -1000,

    minimap_width: 100,
    minimap_height: 200,
    minimap_scale: 10,
    scroll_left: 0,
    scroll_top: 0,
    window_width: 800,
    window_height: 600,
    bounds_margin_horiz: 0,
    bounds_margin_vert: 0,

    editing_artifact_id: null,
    selected_artifacts_dict: {},
    first_selected_artifact: null,
    selection_metrics: {
      contains_text: false,
      contains_images: false,
      contains_audio: false,
      contains_vectors: false,
      contains_shapes: false,
      borders_stylable: true,
      count: 0,
      x: 0,
      y: 0,
      w: 0,
      h: 0,
      x1: 0,
      y1: 0,
      x2: 0,
      y2: 0,
      style: "display:none",
      vector_points: [{},{}],
      vector_selection: false
    },
    selected_artifacts_json: "",
    zones: [],
    user_cursors: [],

    default_style: {}, // will be copied from active_style on startup

    active_style: {
      border_radius: 0,
      stroke: 0,
      font_family: "Inter",
      font_size: 36,
      line_height: 1.5,
      letter_spacing: 0,

      stroke_color: "#000000",
      fill_color: "#00000000",
      text_color: "#000000",
      background_color: "#ffffff",

      padding: 0,
      padding_horz: 0,
      padding_vert: 0,
      padding_top: 0,
      padding_left: 0,
      padding_right: 0,
      padding_bottom: 0,

      margin: 0,
      margin_horz: 0,
      margin_vert: 0,
      margin_top: 0,
      margin_left: 0,
      margin_right: 0,
      margin_bottom: 0,

      brightness: 100,
      contrast: 100,
      opacity: 100,
      saturation: 100,
      blur: 0,
      hue: 0,

      columns: 1,
      column_width: 900,
      row_height: 0,
      gutter: 0,
    },

    color_picker_target: "fill_color",

    color_picker_saturation: 255,
    color_picker_value: 255,
    color_picker_hue: 127,
    color_picker_opacity: 255,

    swatches: [
      {id:1, hex:"#ff00ff"},
      {id:2, hex:"#ffff00"},
      {id:3, hex:"#00ffff"},
      {id:5, hex:"#ff0000"},
      {id:6, hex:"#00ff00"},
      {id:7, hex:"#0000ff"},
      {id:8, hex:"#000000"},
      {id:9, hex:"#222222"},
      {id:10, hex:"#444444"},
      {id:11, hex:"#888888"},
      {id:12, hex:"#bbbbbb"},
      {id:13, hex:"#dddddd"},
      {id:14, hex:"#ffffff"},
      
      {id:20, hex:"#4a2f7e"},
      {id:21, hex:"#9b59b6"},
      {id:22, hex:"#3498db"},
      {id:23, hex:"#2ecc71"},
      {id:24, hex:"#f1c40f"},
      {id:25, hex:"#e67e22"},
      {id:26, hex:"#d55c4b"},
      {id:27, hex:"#6f4021"},
      {id:29, hex:"#95a5a6"},
      {id:30, hex:"rgba(0,0,0,0)"},
    ],
    
    swatches_text: [
      {id:1, hex:"#9b59b6"},
      {id:2, hex:"#3498db"},
      {id:3, hex:"#2ecc71"},
      {id:4, hex:"#f1c40f"},
      {id:5, hex:"#e67e22"},
      {id:6, hex:"#d55c4b"},
      {id:8, hex:"#ffffff"},
      {id:10, hex:"#252525"},
    ],

    fonts: [
      "Inter",
      "Courier"
    ],

    detected_text_formats: {},
    active_text_format_name: "Paragraph",

    image_search_results: [],
    video_search_results: [],
    audio_search_results: [],
    generic_search_query: "",
    media_search_target: "google",
    search_loading: false,

    viewport_zoom: 1,
    viewport_zoom_percent: 100,
    bounds_zoom: 1,

    current_zone_idx: -1,

    margin_mode: "global",
    padding_mode: "global",
    delete_artifact: "unconfirmed",
    color_mode: "palette",
    background_mode: "image",
    layout_mode: "layout",
    follow_mode: true,
    space_background_uploading: false,

    toolbar_props_x: 0,
    toolbar_props_y: 0,
    toolbar_props_in: false,
    toolbar_artifacts_x: "-1000px",
    toolbar_artifacts_y: "-1000px",
    toolbar_artifacts_in: true
  },

  methods: {

    setup_section_module: function() {

      // defaults -----------------------------------------------------------------------------

      this.default_style = _.clone(this.active_style);

      // keybindings --------------------------------------------------------------------------

      Mousetrap.bind('del', function(evt)             { this.if_editable(function() {this.delete_selected_artifacts(evt);}) }.bind(this));
      Mousetrap.bind('backspace', function(evt)       { this.if_editable(function() {this.delete_selected_artifacts(evt);}) }.bind(this));
      Mousetrap.bind(['command+d',      'ctrl+d'      ], function(evt) { evt.preventDefault(); evt.stopPropagation(); this.if_editable(function() {this.duplicate_selected_artifacts();}) }.bind(this));
      Mousetrap.bind(['command+z',      'ctrl+z'      ], function(evt) { this.if_editable(function() {this.undo();}) }.bind(this));
      Mousetrap.bind(['command+shift+z','ctrl+shift+z'], function(evt) { this.if_editable(function() {this.redo();}) }.bind(this));
      Mousetrap.bind(['command+a',      'ctrl+a'      ], function(evt) { this.if_editable(function() {this.select_all_artifacts();}) }.bind(this));
      Mousetrap.bind(['command+e',      'ctrl+e'      ], function(evt) { this.if_editable(function() {this.toggle_full_width();}) }.bind(this));
      Mousetrap.bind(['command+=',      'ctrl+='      ], function(evt) { evt.preventDefault(); evt.stopPropagation(); this.zoom_in();  }.bind(this));
      Mousetrap.bind(['command+-',      'ctrl+-'      ], function(evt) { evt.preventDefault(); evt.stopPropagation(); this.zoom_out(); }.bind(this));
      Mousetrap.bind('+', function(evt)               { evt.preventDefault(); evt.stopPropagation(); this.zoom_in();  }.bind(this));
      Mousetrap.bind('-', function(evt)               { evt.preventDefault(); evt.stopPropagation(); this.zoom_out(); }.bind(this));
      Mousetrap.bind('up', function(evt)              { this.nudge_selected_artifacts(0,-1,evt);}.bind(this));
      Mousetrap.bind('down', function(evt)            { this.nudge_selected_artifacts(0,1,evt);}.bind(this));
      Mousetrap.bind('left', function(evt)            { this.nudge_selected_artifacts(-1,0,evt);}.bind(this));
      Mousetrap.bind('right', function(evt)           { this.nudge_selected_artifacts(1,0,evt);}.bind(this));
      Mousetrap.bind('shift+up', function(evt)        { this.if_editable(function() {this.nudge_selected_artifacts(0,-10,evt);}) }.bind(this));
      Mousetrap.bind('shift+down', function(evt)      { this.if_editable(function() {this.nudge_selected_artifacts(0,10,evt);}) }.bind(this));
      Mousetrap.bind('shift+left', function(evt)      { this.if_editable(function() {this.nudge_selected_artifacts(-10,0,evt);}) }.bind(this));
      Mousetrap.bind('shift+right', function(evt)     { this.if_editable(function() {this.nudge_selected_artifacts(10,0,evt);}) }.bind(this));
      Mousetrap.bind('space', function(evt)           { this.activate_pan_tool(evt); }.bind(this));
      
      $(document).bind("beforecopy", this.handle_onbeforecopy.bind(this));
      $(window).bind("beforeunload", this.handle_onunload.bind(this));
      $(window).bind("resize", this.handle_window_resize.bind(this));
    },

    setup_watches: function() {
      this.$watch('active_style.stroke', function (value, mutation) {
        this.set_artifact_style_prop("stroke",parseInt(this.active_style.stroke));
      }.bind(this));

      this.$watch('active_style.border_radius', function (value, mutation) {
        this.set_artifact_style_prop("border_radius",parseInt(this.active_style.border_radius));
      }.bind(this));

      this.$watch('active_style.padding', function (value, mutation) {
        this.active_style.padding_horz = this.active_style.padding;
        this.active_style.padding_vert = this.active_style.padding;
      }.bind(this));
      this.$watch('active_style.padding_horz', function (value, mutation) {
        this.active_style.padding_left = this.active_style.padding_horz;
        this.active_style.padding_right = this.active_style.padding_horz;
      }.bind(this));
      this.$watch('active_style.padding_vert', function (value, mutation) {
        this.active_style.padding_top = this.active_style.padding_vert;
        this.active_style.padding_bottom = this.active_style.padding_vert;
      }.bind(this));

      this.$watch('active_style.padding_top', function (value, mutation) {
        this.set_artifact_style_prop("padding_top",parseInt(this.active_style.padding_top));
      }.bind(this));
      this.$watch('active_style.padding_bottom', function (value, mutation) {
        this.set_artifact_style_prop("padding_bottom",parseInt(this.active_style.padding_bottom));
      }.bind(this));
      this.$watch('active_style.padding_left', function (value, mutation) {
        this.set_artifact_style_prop("padding_left",parseInt(this.active_style.padding_left));
      }.bind(this));
      this.$watch('active_style.padding_right', function (value, mutation) {
        this.set_artifact_style_prop("padding_right",parseInt(this.active_style.padding_right));
      }.bind(this));

      this.$watch('active_style.margin', function (value, mutation) {
        this.active_style.margin_horz = this.active_style.margin;
        this.active_style.margin_vert = this.active_style.margin;
      }.bind(this));

      this.$watch('active_style.margin_horz', function (value, mutation) {
        this.active_style.margin_left = this.active_style.margin_horz;
        this.active_style.margin_right = this.active_style.margin_horz;
      }.bind(this));
      this.$watch('active_style.margin_vert', function (value, mutation) {
        this.active_style.margin_top = this.active_style.margin_vert;
        this.active_style.margin_bottom = this.active_style.margin_vert;
      }.bind(this));

      this.$watch('active_style.margin_top', function (value, mutation) {
        this.set_artifact_style_prop("margin_top",parseInt(this.active_style.margin_top));
      }.bind(this));
      this.$watch('active_style.margin_bottom', function (value, mutation) {
        this.set_artifact_style_prop("margin_bottom",parseInt(this.active_style.margin_bottom));
      }.bind(this));
      this.$watch('active_style.margin_left', function (value, mutation) {
        this.set_artifact_style_prop("margin_left",parseInt(this.active_style.margin_left));
      }.bind(this));
      this.$watch('active_style.margin_right', function (value, mutation) {
        this.set_artifact_style_prop("margin_right",parseInt(this.active_style.margin_right));
      }.bind(this));

      this.$watch('active_style.stroke_color', function (value, mutation) {
        this.set_artifact_style_prop("stroke_color",this.active_style.stroke_color);

        var rgba = hex_to_rgba(this.active_style.stroke_color);
        var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);
        this.active_style.stroke_color_hsv = hsv;
      }.bind(this));

      this.$watch('active_style.fill_color', function (value, mutation) {
        this.set_artifact_style_prop("fill_color",this.active_style.fill_color);

        var rgba = hex_to_rgba(this.active_style.fill_color);
        var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);
        this.active_style.fill_color_hsv = hsv;
      }.bind(this));

      this.$watch('active_style.text_color', function (value, mutation) {
        this.set_artifact_style_prop("text_color",this.active_style.text_color);
        this.apply_formatting(null,"forecolor",this.active_style.text_color);

        var rgba = hex_to_rgba(this.active_style.text_color);
        var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);
        this.active_style.text_color_hsv = hsv;
      }.bind(this));

      this.$watch('active_style.font_size', function (value, mutation) {
        this.apply_formatting(null,"preciseFontSize",this.active_style.font_size+"px");
      }.bind(this));

      this.$watch('active_style.line_height', function (value, mutation) {
        this.apply_formatting(null,"lineHeight",this.active_style.line_height+"em");
      }.bind(this));

      this.$watch('active_style.letter_spacing', function (value, mutation) {
        this.apply_formatting(null,"letterSpacing",this.active_style.letter_spacing+"px");
      }.bind(this));

      // color picker

      this.$watch('color_picker_hue', function (value, mutation) {
        this.apply_color_picker();
      }.bind(this));

      this.$watch('color_picker_value', function (value, mutation) {
        this.apply_color_picker();
      }.bind(this));

      this.$watch('color_picker_saturation', function (value, mutation) {
        this.apply_color_picker();
      }.bind(this));

      this.$watch('color_picker_opacity', function (value, mutation) {
        this.apply_color_picker();
      }.bind(this));

      // filters

      this.$watch('active_style.brightness', function (value, mutation) {
        this.set_artifact_style_prop("brightness",parseInt(this.active_style.brightness));
      }.bind(this));

      this.$watch('active_style.blur', function (value, mutation) {
        this.set_artifact_style_prop("blur",parseInt(this.active_style.blur));
      }.bind(this));

      this.$watch('active_style.contrast', function (value, mutation) {
        this.set_artifact_style_prop("contrast",parseInt(this.active_style.contrast));
      }.bind(this));

      this.$watch('active_style.saturation', function (value, mutation) {
        this.set_artifact_style_prop("saturation",parseInt(this.active_style.saturation));
      }.bind(this));

      this.$watch('active_style.hue', function (value, mutation) {
        this.set_artifact_style_prop("hue",parseInt(this.active_style.hue));
      }.bind(this));

      this.$watch('active_style.opacity', function (value, mutation) {
        this.set_artifact_style_prop("opacity",parseInt(this.active_style.opacity));
      }.bind(this));

      this.throttled_save_active_space = _.throttle(function(){
        save_space(this.active_space);
      }.bind(this), 2000);

      // canvas
      this.$watch('active_style.background_color', function (value, mutation) {

        if (this.active_style.background_color != this.active_space.background_color) {
          this.$set("active_space.background_color",this.active_style.background_color);
          this.throttled_save_active_space();
        }

        var rgba = hex_to_rgba(this.active_style.background_color);
        var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);
        this.active_style.background_color_hsv = hsv;
      }.bind(this));
    },

    if_editable: function(fn) {
      // call given closure if space is editable
      // used by key bindings
      if (this.active_space_role!="viewer") fn.bind(this)();
    },

    background_image_style: function(images) {
      if (!images) return null;
      if (isNaN(images.length)) images = [images];

      for (var i=0; i<images.length; i++) {
        if (images[i] && images[i].length>0) {
          return "background-image: url("+images[i]+")";
        }
      }
    },

    space_thumbnail_style: function(space) {
      if (space.avatar_thumb_uri && space.avatar_thumb_uri.length>0) {
        return "background-image:url('"+space.avatar_thumb_uri+"')";
      }
      if (space.space_type == "folder") return "";

      return "background-image:url('/api/spaces/"+space._id+"/png')";
    },

    reset_artifact_filters: function() {
      this.active_style.brightness = this.default_style.brightness;
      this.active_style.contrast = this.default_style.contrast;
      this.active_style.opacity = this.default_style.opacity;
      this.active_style.saturation = this.default_style.saturation;
      this.active_style.blur = this.default_style.blur;
      this.active_style.hue = this.default_style.hue;
    },

    increase_columns: function() {
      if (this.active_style.columns<this.MAX_COLUMNS) this.active_style.columns++;
    },

    decrease_columns: function() {
      if (this.active_style.columns>1) this.active_style.columns--;
    },

    extract_properties_from_selection: function() {
      // stop extract->apply feedback loop
      this.skip_formatting = true;
      
      var arts = this.selected_artifacts();
      window.setTimeout(function() {
        this.skip_formatting = false;
      }.bind(this),10);

      if (!arts.length) return;

      if (arts.length == 1) {
        var a = arts[0];

        var props = [
          "stroke",
          "border_radius",
          "letter_spacing",
          "stroke_color",
          "fill_color",
          "text_color"
        ];

        for (var i=0; i<props.length; i++) {
          var prop = props[i];
          this.active_style[prop]=a[prop];
        }

        // defaults
        //this.active_style.padding = this.default_style.padding;
        this.active_style.font_size = this.default_style.font_size;
        this.active_style.line_height = this.default_style.line_height;
        this.active_style.letter_spacing = this.default_style.letter_spacing;

        this.active_style.padding_top =    a.padding_top || 0;
        this.active_style.padding_bottom = a.padding_bottom || 0;
        this.active_style.padding_left =   a.padding_left || 0;
        this.active_style.padding_right =  a.padding_right || 0;

        if (this.active_style.padding_top == this.active_style.padding_bottom) {
          this.active_style.padding_vert = this.active_style.padding_top;
        }

        if (this.active_style.padding_left == this.active_style.padding_right) {
          this.active_style.padding_horz = this.active_style.padding_left;
        }

        if (this.active_style.padding_top  == this.active_style.padding_bottom &&
            this.active_style.padding_left == this.active_style.padding_right &&
            this.active_style.padding_left == this.active_style.padding_top) {
          this.active_style.padding = this.active_style.padding_top;
        }

        this.active_style.margin_top =    a.margin_top || 0;
        this.active_style.margin_bottom = a.margin_bottom || 0;
        this.active_style.margin_left =   a.margin_left || 0;
        this.active_style.margin_right =  a.margin_right || 0;

        if (this.active_style.margin_top == this.active_style.margin_bottom) {
          this.active_style.margin_vert = this.active_style.margin_top;
        }

        if (this.active_style.margin_left == this.active_style.margin_right) {
          this.active_style.margin_horz = this.active_style.margin_left;
        }

        if (this.active_style.margin_top  == this.active_style.margin_bottom &&
            this.active_style.margin_left == this.active_style.margin_right &&
            this.active_style.margin_left == this.active_style.margin_top) {
          this.active_style.margin = this.active_style.margin_top;
        }
      }

      this.update_selection_metrics();

      // extract text styles
      this.selection_metrics.contains_text = false;
      this.selection_metrics.contains_images = false;
      this.selection_metrics.contains_audio = false;
      this.selection_metrics.contains_embeds = false;
      this.selection_metrics.contains_vectors = false;
      this.selection_metrics.contains_shapes = false;
      this.selection_metrics.borders_stylable = false;

      var notes = _.filter(arts, function(a) { return (a.mime=="text/html" || a.mime=="x-spacedeck/shape") });

      if (notes.length>=1) {
        this.selection_metrics.contains_text = true;

        if (notes.length==1) {
          var a = notes[0];
          var dom = $("<div>"+a.description+"</div>")[0];
          var el = dom.firstChild;

          do {
            if (el && el.style) {
              if (el.style.fontSize) this.active_style.font_size = parseInt(el.style.fontSize);
              if (el.style.fontFamily) this.active_style.font_family = el.style.fontFamily;
              if (el.style.letterSpacing) this.active_style.letter_spacing = parseInt(el.style.letterSpacing);
              if (el.style.lineHeight) this.active_style.line_height = parseFloat(el.style.lineHeight);
              if (el.style.color) this.active_style.text_color = el.style.color;
            }
          } while (el && (el = dom.nextSibling));
        }
      }

      if (arts.length == 1) {
        this.extract_color_picker_from_selection();
      }

      var images = _.filter(arts, function(a) { return a.mime.match("image") });
      if (images.length>=1) {
        this.selection_metrics.contains_images = true;
      }

      var audio = _.filter(arts, function(a) { return a.mime.match("audio") });
      if (audio.length>=1) {
        this.selection_metrics.contains_audio = true;
      }

      var embeds = _.filter(arts, function(a) { return a.mime.match("embed") });
      if (embeds.length>=1) {
        this.selection_metrics.contains_embeds = true;
      }

      var embeds = _.filter(arts, function(a) { return a.mime=="x-spacedeck/vector" });
      if (embeds.length>=1) {
        this.selection_metrics.contains_vectors = true;
      }

      var embeds = _.filter(arts, function(a) { return a.mime=="x-spacedeck/shape"; });
      if (embeds.length>=1) {
        this.selection_metrics.contains_shapes = true;
      }

      var sm = this.selection_metrics;
      this.selection_metrics.borders_stylable = !(sm.contains_vectors||sm.contains_shapes);
    },

    increase_letter_spacing: function(evt) {
      this.active_style.letter_spacing++;
    },

    decrease_letter_spacing: function(evt) {
      this.active_style.letter_spacing--;
    },

    apply_font: function(evt, font) {
      this.apply_formatting(evt,'fontName',font);
      this.active_style.font_family = font;
    },

    toggle_advanced_properties: function() {
      this.advanced_properties = !this.advanced_properties;
    },

    open_dialog: function(id, evt) {
      if (evt) {
        evt.stopPropagation();
        evt.preventDefault();
      }

      this.active_tool = "pointer";

      if (this.opened_dialog == id) {
        this.opened_dialog = "none";
        return;
      }

      if (_.contains(["mobile","shapes","zones"],id)) {
        this.deselect();
      }
      
      this.opened_dialog=id;

      if (id.match("color") || id.match("background")) {
        this.color_picker_target = id.replace("color-","")+"_color";
        this.color_mode = "palette";
        this.extract_color_picker_from_selection();
      }

      if (_.contains(["audio","video","image","search"],id)) {
        if ($("#"+id+" input")[0]) {
          $("#"+id+" input")[0].focus();
        }
      }

      if (this.opened_dialog == "background") {
        this.color_picker_target = "background_color";
        this.background_mode='color';
      }

      if (this.opened_dialog == "info") {
        this.access_settings_space = this.active_space;
        this.access_settings_memberships = this.active_space_memberships;
        this.editors_section = "list";

        if (this.active_space_is_readonly || this.embedded) {
          this.space_info_section = "info";
        } else if (this.active_space_role == "admin") {
          this.space_info_section = "access";
        }
      }
    },

    toggle_color_options: function() {
      this.color_options_picker = !this.color_options_picker;
    },

    close_lightbox: function() {
      this.lightbox_artifact = {};
      this.close_modal();
    },

    /* --------------- artifact manipulation ------------------------- */

    prepare_clipboard: function() {
      if ('ontouchstart' in window) return; // don't do this on touch devices

      this.selected_artifacts_json = JSON.stringify(this.selected_artifacts());
      //$("#space-clipboard > textarea")[0].blur();

      this.prepare_clipboard_step2();
    },

    prepare_clipboard_step2: function() {
      if ('ontouchstart' in window) return; // don't do this on touch devices

      setTimeout(function() {
        if (!$("#space-clipboard > textarea").length) return; // not ready yet

        $("#space-clipboard > textarea")[0].focus();
        $("#space-clipboard > textarea")[0].select();
      },100);
    },

    handle_section_keydown: function(evt) {
      if (evt.keyCode == 67 && (evt.ctrlKey || evt.metaKey)) { // c key
        this.prepare_clipboard();
        this.prepare_clipboard_step2();
      }
      return true;
    },

    handle_onbeforecopy: function(evt) {
      if (this.editing_artifact_id) return;

      var focused_tag = evt.target.nodeName.toLowerCase();
      if (focused_tag != "body") return;

      this.prepare_clipboard_step2();

      window.setTimeout(function() {
        if (!$("#space-clipboard > textarea").length) return; // not ready yet
        $("#space-clipboard > textarea")[0].blur();
      },10);
    },

    handle_onunload: function(evt) {
      if (!window.artifact_save_queue) return;

      var changes = Object.keys(window.artifact_save_queue).length;

      if (changes>0) {
        var message = "There are "+changes+" changes that are still being saved. Discard them?";
        evt.returnValue = message;
        return message;
      }

      window._spacedeck_location_change = true;
    },

    handle_window_resize: function(evt) {
      this.adjust_bounds_zoom();
    },

    handle_scroll: function(evt) {
      if (this.active_view!="space") return;
      if (!$("#space").length) return;

      el = $("#space")[0];
      this.scroll_left   = el.scrollLeft/this.viewport_zoom;
      this.scroll_top    = el.scrollTop/this.viewport_zoom;
      this.window_width  = window.innerWidth/this.viewport_zoom;
      this.window_height = window.innerHeight/this.viewport_zoom;

      this.resize_minimap();

      // follow presenter mode: send viewport rectangle to viewers
      if (this.logged_in && this.present_mode) {
        if (this.active_space_role!="viewer") {
          this.presenter_send_viewport();
        }
      }
    },

    presenter_send_viewport: function() {
      name = this.user.nickname || this.user.email;
      
      var msg = {
        action: "viewport",
        x: this.scroll_left,
        y: this.scroll_top,
        w: this.window_width,
        h: this.window_height,
        zoom: this.viewport_zoom,
        name: name,
        id: this.user._id
      };

      var packed = JSON.stringify(msg);
      if (packed==this._old_viewport_msg) return;
      this._old_viewport_msg = packed;
      
      if (this.present_mode && this.active_space_role!="viewer")
        this.websocket_send(msg);
    },

    presenter_send_media_action: function(artifact_id,type,cmd,time) {
      name = this.user.nickname || this.user.email;
      
      var msg = {
        action: "media",
        artifact_id: artifact_id,
        type: type,
        command: cmd,
        time: time,
        name: name,
        id: this.user._id
      };
      if (this.present_mode && this.active_space_role!="viewer")
        this.websocket_send(msg);
    },

    resize_minimap: function() {
      if (!this.active_space) return;
      this.minimap_scale = this.active_space.width/100.0;
    },

    handle_minimap_mouseup: function(evt) {
      this.minimap_mouse_state = "idle";
    },

    handle_minimap_mousemove: function(evt) {
      if (this.minimap_mouse_state=="pressed") {
        this.handle_minimap_mousedown(evt);
      }
    },

    handle_minimap_mousedown: function(evt) {
      if (!$("#space").length) return;

      this.minimap_mouse_state = "pressed";
      el = $("#space")[0];

      evt = fixup_touches(evt);

      var ofs = $(evt.target).offset();
      var x = evt.pageX - ofs.left;
      var y = evt.pageY - ofs.top;

      el.scrollLeft = (x-this.window_width/(this.minimap_scale*2))*this.minimap_scale*this.viewport_zoom;
      el.scrollTop  = (y-this.window_height/(this.minimap_scale*2))*this.minimap_scale*this.viewport_zoom;
      this.handle_scroll();
    },

    handle_user_cursor_update: function(msg) {
      // console.log("handle cursor", msg);
      var now = new Date().getTime();
      msg.t = now;
      var existing = false;
      for (var i=0; i<this.user_cursors.length; i++) {
        var u = this.user_cursors[i];

        if (u.id == msg.id) {
          u.x = msg.x;
          u.y = msg.y;
          u.t = now;
          u.name = msg.name;
          // console.log("updated cursor "+i);
          existing = true;
        } else {
          // hide if no updates since 2sec
          if ((now-u.t)>5000) {
            u.x=-10000;
          }
        }
      }
      if (!existing) {
        this.user_cursors.push(_.clone(msg));
      }
    },

    handle_presenter_viewport_update: function(msg) {
      this.zoom_to_rect({
        x1: msg.x,
        y1: msg.y,
        x2: msg.x+msg.w,
        y2: msg.y+msg.h
      });
    },
    
    handle_presenter_media_update: function(msg) {
      if(this.follow_mode) {
        if (msg.type=="audio") {
          var sel="#artifact-"+msg.artifact_id+" .audio";
          try {
            $(sel)[0].dispatchEvent(new Event("remote_"+msg.command));
            console.log("event dispatched");
          } catch (e) {
          }
        }
        if (msg.type=="video") {
          var sel="#artifact-"+msg.artifact_id+" .video";
          try {
            $(sel)[0].dispatchEvent(new Event("remote_"+msg.command));
            console.log("event dispatched");
          } catch (e) {
          }
        }
      } else {
        console.log("ignore media update, muted");
      }
    },

    may_select: function(a) {
      if (!a) return false;
      if (!this.active_space) return false;
      
      if (this.active_space_role=="viewer" || (a.locked && this.active_space_role!="admin")) {
        return false;
      }

      if (this.active_space.editors_locking && !this.logged_in && this.guest_nickname!=a.editor_name) {
        return false;
      }
      return true;
    },

    select: function(evt, a) {
      if (!this.may_select(a)) return;

      if (evt && !evt.shiftKey && this.is_selected(a)) return; // already selected

      if (!evt || !evt.shiftKey) {
        this.deselect();
      }

      if (evt && evt.shiftKey) {
        if (this.selected_artifacts_dict[a._id]) {
          delete this.selected_artifacts_dict[a._id];
        } else {
          this.selected_artifacts_dict[a._id] = true;
        }
      } else {
        this.selected_artifacts_dict[a._id] = true;
      }

      this.update_board_artifact_viewmodel(a);
      this.extract_properties_from_selection();
      this.update_selection_metrics();
      this.prepare_clipboard();
      this.show_toolbar_props();
    },

    select_all_artifacts: function(evt) {
      this.deselect();
      for (var i=0; i<this.active_space_artifacts.length; i++) {
        var a = this.active_space_artifacts[i];
        if (this.may_select(a)) {
          this.selected_artifacts_dict[a._id] = true;
          this.update_board_artifact_viewmodel(a);
        }
      }

      this.update_selection_metrics();
      this.extract_properties_from_selection();
      this.prepare_clipboard();
      this.show_toolbar_props();
    },

    multi_select: function(arts) {
      for (var i=0; i<arts.length; i++) {
        var a = arts[i];
        if (this.may_select(a)) {
          this.selected_artifacts_dict[a._id] = true;
          this.update_board_artifact_viewmodel(a);
        }
      }

      this.extract_properties_from_selection();
      this.update_selection_metrics();
      this.prepare_clipboard();
      this.show_toolbar_props();
    },

    discover_zones: function() {
      this.zones = _.sortBy(_.filter(this.active_space_artifacts, function(a) { return (a.mime=="x-spacedeck/zone") }),
        function(z){return z.order});
    },

    artifact_plaintext: function(a) {
      if (!a) return "";
      var txt = $("<div>"+a.description+"</div>").text();
      return txt || "";
    },

    deselect: function(hard) {
      if (window._sd_fader_moving) {
        window._sd_fader_moving = false; // signal from fader directive
        return;
      }

      this.hide_toolbar_props();

      document.getSelection().removeAllRanges();

      blur();
      //this.prepare_clipboard();
      this.prepare_clipboard_step2();
      this.discover_zones();

      var prev_selected = this.selected_artifacts();
      this.selected_artifacts_dict = {};

      // nuke empty notes
      for (var i=0; i<prev_selected.length; i++) {
        var a = prev_selected[i];
        var keep = true;
        if (a && a.mime == "text/html") {
          var txt = this.artifact_plaintext(a);
          if (txt.length == 0) {
            keep = true; //Quick Fix For yassin, complains about buggy behaviour
          }
        }

        if (!keep) {
          this.selected_artifacts_dict[a._id] = a;
        }
      }
      this.delete_selected_artifacts(null, true); // delete all selected in loop

      this.selected_artifacts_dict = {};
      this.editing_artifact_id = null;
      this.opened_dialog = "none";

      for (var i=0; i<prev_selected.length; i++) {
        this.update_board_artifact_viewmodel(prev_selected[i]);
      }

      /*this.selection_metrics.x=0;
      this.selection_metrics.y=0;
      this.selection_metrics.w=0;
      this.selection_metrics.h=0;
      this.selection_metrics.style="display:none";*/
      this.selection_metrics.contains_text=false;
      this.selection_metrics.count=0;

      if (hard) {
        this.active_tool = "pointer";
        this.mouse_state = "idle";
      }

      this.update_selection_metrics();
    },

    is_selected: function(a) {
      if (!a) return;
      return (!!this.selected_artifacts_dict[a._id]);
    },

    unselected_artifacts: function() {
      return this.active_space_artifacts.filter(function(a){return !this.is_selected(a)}.bind(this));
    },


    selection_rect_style: function() {
      var r = this.selection_rect();
      if (r==null) return "display:none";

      return "left:"+r.x1+"px;top:"+r.y1+"px;width:"+(r.x2-r.x1)+"px;height:"+(r.y2-r.y1)+"px;";
    },

    selection_rect: function() {
      return this.enclosing_rect(this.selected_artifacts());
    },

    enclosing_rect: function(arts) {
      if (arts.length==0) return null;
      arts = _.filter(arts); // remove any nulls

      return {
        x1: parseInt(_.min(arts.map(function(a){return ((!a || !a.x)?0:a.x)}))),
        y1: parseInt(_.min(arts.map(function(a){return ((!a || !a.y)?0:a.y)}))),
        x2: parseInt(_.max(arts.map(function(a){return (!a?0:a.x+a.w)}))),
        y2: parseInt(_.max(arts.map(function(a){return (!a?0:a.y+a.h)})))
      };
    },

    update_selection_metrics: function(arts) {

      if (this.active_tool == "scribble") {
        this.selection_metrics.count = 1;
        return;
      }
      
      var sr = this.selection_rect() || {x:0,y:0,w:0,h:0,style:"display:none"};

      if (sr.x1 || sr.x2) {
        sr.w = sr.x2-sr.x1;
        sr.h = sr.y2-sr.y1;

        sr.style = this.selection_rect_style();

        var pp = this.space_point_to_window(sr.x1+sr.w/2,sr.y2);
        var pp2 = this.space_point_to_window(sr.x1+sr.w/2,sr.y1);

        pp.x-=260;
        pp.y-=10;

        if (pp.y>=window.innerHeight-300) {
          pp.y = pp2.y-100;
        }
        if (pp.x<0) {
          pp.x=0;
        }
        if (pp.y<0) {
          pp.y=0;
        }

        // FIXME make sure that menus fit in window
        this.toolbar_props_x = pp.x+"px";
        this.toolbar_props_y = pp.y+"px";
          
        //this.hide_toolbar_artifacts();
      }

      this.selection_metrics.x1 = sr.x1;
      this.selection_metrics.x2 = sr.x2;
      this.selection_metrics.y1 = sr.y1;
      this.selection_metrics.y2 = sr.y2;
      this.selection_metrics.x = sr.x;
      this.selection_metrics.y = sr.y;
      this.selection_metrics.w = sr.w;
      this.selection_metrics.h = sr.h;
      this.selection_metrics.style = sr.style;

      if (!arts) arts = this.selected_artifacts();

      this.first_selected_artifact = arts[0];
      this.selection_metrics.count=arts.length;
      this.selection_metrics.scribble_selection = false;
      if (arts.length == 1 && arts[0].mime == "x-spacedeck/vector") {
        if (arts[0].shape == "scribble") {
          this.selection_metrics.scribble_selection = true;
        }
        this.selection_metrics.vector_points = arts[0].control_points;
        this.selection_metrics.vector_selection = true;
      } else {
        this.selection_metrics.vector_points = [{},{}];
        this.selection_metrics.vector_selection = false;
      }
      this.selection_metrics.has_link=false;
      this.insert_link_url="";
      if (arts.length == 1 && arts[0].meta && arts[0].meta.link_uri && arts[0].meta.link_uri.length>0) {
        this.selection_metrics.has_link=true;
        this.insert_link_url = arts[0].meta.link_uri;
      }
    },

    begin_transaction: function() {
      this.transaction_running = true;

      if (!this.undo_stack.length || this.undo_stack[this.undo_stack.length-1].action!="empty") {
        this.undo_stack.push({action:"empty"});
      } else {
        //console.log("undo slot is already empty.");
      }

      this.redo_stack = [];

      this.artifacts_before_transaction = this.active_space_artifacts.map(function(a) {
        return _.cloneDeep(a);
      });
    },

    fixup_space_size: function() {
      if (!this.active_space) return;

      this.active_space.width =Math.max(this.active_space.width, window.innerWidth);
      this.active_space.height=Math.max(this.active_space.height, window.innerHeight);
    },

    end_transaction: function() {
      this.transaction_running = false;
      this.throttled_process_artifact_save_queue();

      if (!this.active_space) return;

      var er = this.enclosing_rect(this.active_space_artifacts);
      if (!er) return;

      // resize space
      this.active_space.width =Math.max((parseInt(er.x2/window.innerWidth)+2)*window.innerWidth, window.innerWidth);
      this.active_space.height=Math.max((parseInt(er.y2/window.innerHeight)+2)*window.innerHeight, window.innerHeight);

      console.log("bounds: ",this.active_space.width,this.active_space.height);
      
      if (this._last_bounds_width != this.active_space.width ||
        this._last_bounds_height != this.active_space.height) {
        this._last_bounds_width = this.active_space.width;
        this._last_bounds_height = this.active_space.height;

        save_space(this.active_space);
      }

      this.resize_minimap();
      this.discover_zones();
    },

    find_artifact_before_transaction: function(needle) {
      return this.find_artifact_in_array(this.artifacts_before_transaction, needle);
    },

    find_artifact_in_array: function(haystack, needle) {
      var res = _.find(haystack, function(a) {
        return (needle._id && (a._id == needle._id));
      });
      return res;
    },

    unsaved_transactions: function() {
      if (!window.artifact_save_queue) return 0;
      return Object.keys(window.artifact_save_queue).length;
    },

    process_artifact_save_queue: function() {
      if (!window.artifact_save_queue) {
        return;
      }

      if (this.transaction_running) {
        console.log("not saving, transaction still in progress.");
        return;
      }

      var ids = Object.keys(window.artifact_save_queue);

      for (var i=0; i<ids.length; i++) {
        var id = ids[i];
        var a = window.artifact_save_queue[id];

        if (this.guest_nickname) {
          a.editor_name = this.guest_nickname;
        }

        save_artifact(a, function() {
          delete window.artifact_save_queue[id];
        }.bind(this), function(req) {
          if (req && req.status == 404) {
            // artifact was already deleted, ignore
            delete window.artifact_save_queue[id];
          } else {
            console.log("could not save artifact, will try again:",a,req);
          }
        });
      }

      // mark version dirty locally
      if (this.active_space) {
        this.active_space.updated_at = (new Date()).getTime();
      }

      //window.artifact_save_queue = {};
    },

    throttled_process_artifact_save_queue: function() {
      if (!this._throttled_process_artifact_save_queue) {
        this._throttled_process_artifact_save_queue = _.throttle(this.process_artifact_save_queue, 500);
      }
      this._throttled_process_artifact_save_queue();
    },

    queue_artifact_for_save: function(a) {
      if (!window.artifact_save_queue) {
        window.artifact_save_queue = {};
      }

      if (!a._id) {
        console.log("warning: illegal artifact queued for save");
      }

      // this is a bit hacky, but might be the smartest place to do it
      if (a.view && a.view.vector_svg) {
        a.shape_svg = a.view.vector_svg;
      }

      window.artifact_save_queue[a._id] = a;
    },

    update_properties: function(artifact_ids, changes) {
      for (var i=0; i<artifact_ids.length; i++) {
        var id = artifact_ids[i];

        var a = this.find_artifact_by_id(id);
        if (a) {
          var changed = false;

          for (k in changes[i]) {
            //console.log("change: ",k,": ",changes[i][k],"<-",a[k])
            a[k] = changes[i][k];
            changed = true;
          }

          this.update_board_artifact_viewmodel(a);

          // TODO: throttle, bundle etc
          if (changed) {
            this.queue_artifact_for_save(a);
          }
        }
      }
    },

    update_artifacts: function(artifacts, change_func) {
      var artifact_ids = [];
      var changes = [];

      for (var i=0; i<artifacts.length; i++) {
        var a = artifacts[i];
        var c = change_func(a);
        if (c) {
          artifact_ids.push(a._id);
          changes.push(c);
        }
      }

      if (changes.length) {
        this.push_to_undo({
          action: "update",
          artifact_ids: artifact_ids,
          changes: changes,
          snapshot: this.artifacts_before_transaction
        });
      }

      if (changes.length) {
        this.update_properties(artifact_ids, changes);
      }
    },

    push_to_undo: function(transaction) {
      // begin_transaction opens new undo slot
      // push_to_undo replaces the current undo slot

      this.undo_stack[this.undo_stack.length-1] = transaction;
      //console.log(transaction.action+"; undo_stack len: ",this.undo_stack.length);
    },

    undo: function() {
      if (!this.undo_stack.length || this.undo_stack[this.undo_stack.length-1].action=="empty") {
        console.log("nothing to undo!");
        return;
      }

      var step = this.undo_stack.pop();
      console.log("undo popped: ",step);
      this.redo_stack.push(step);

      for (var i=0; i<step.artifact_ids.length; i++) {
        var id = step.artifact_ids[i];
        var old_version = this.find_artifact_in_array(step.snapshot, {_id:id});

        if (step.action == "update") {
          if (old_version) {
            this.update_properties([id], [old_version]);
          }
        } else {
          delete old_version._id;
          save_artifact(old_version, function(restored_a) {
            this.update_board_artifact_viewmodel(restored_a);
            this.active_space_artifacts.push(restored_a);
            // TODO: rewrite undo history's artifact ids
          }.bind(this));
        }
      }
      this.update_selection_metrics();
    },

    redo: function() {
      if (!this.redo_stack.length) {
        console.log("nothing to redo!");
        return;
      }

      var step = this.redo_stack.pop();
      console.log("redo popped: ",step);
      this.undo_stack.push(step);

      this.update_properties(step.artifact_ids, step.changes);
      this.update_selection_metrics();
    },

    set_artifact_prop: function(prop, val) {
      this.begin_transaction();

      this.update_selected_artifacts(function(a) {
        var c = {};

        if (a[prop] != val) {
          //console.log("set_artifact_prop: ",c,val);
          c[prop]=val;
          return c;
        }

        return null;
      });
    },

    set_artifact_style_prop: function(prop, val) {
      this.begin_transaction();

      this.update_selected_artifacts(function(a) {
        var c = {};
        
        if (a[prop] != val) {
          //console.log("set_artifact_style_prop: ",c,val);
          c[prop]=val;
          return c;
        }

        return null;
      });
    },

    activate_color_mode: function(mode) {
      this.color_mode = mode;
      if (mode == 'picker') {
        // default to full alpha if color is 0,0,0,0
        if (this.color_picker_hue == 0 &&
          this.color_picker_saturation == 0 &&
          this.color_picker_value == 0 &&
          this.color_picker_opacity == 0) {
          this.color_picker_opacity = 255;
          this.color_picker_value = 255;
        }
      }
    },

    reset_stroke: function() {
      this.active_style.stroke = 0;
      this.active_style.border_radius = 0;
      this.active_style.stroke_style = "solid";
    },

    apply_font_size: function(px) {
      this.apply_formatting(null,"preciseFontSize",px+"px");
    },

    apply_swatch_color: function(swatch) {

      var rgba = hex_to_rgba(swatch.hex);

      var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);

      this.color_picker_hue = parseFloat(hsv.h*255);
      this.color_picker_saturation = parseFloat(hsv.s*255);
      this.color_picker_value = parseFloat(hsv.v*255);
      this.color_picker_opacity = rgba.a*255;
      this.color_picker_rgb = rgb_to_hex(rgba.r,rgba.g,rgba.b);

      //console.log("swatch hex: ",swatch.hex);

      this.active_style[this.color_picker_target] = swatch.hex;

      if (this.color_picker_target == "stroke_color") {
        if (!this.active_style.stroke) {
          // set a default stroke for convenience
          this.active_style.stroke = 2;
        }
      }
    },

    apply_color_picker: function() {
      var rgb = hsv_to_rgb(this.color_picker_hue/255,
        this.color_picker_saturation/255,
        this.color_picker_value/255);
      var alpha = this.color_picker_opacity/255;

      //console.log("apply_color_picker: ",rgb,alpha);

      this.active_style[this.color_picker_target] = "rgba("+[rgb.r,rgb.g,rgb.b,alpha].join(",")+")";
    },

    extract_color_picker_from_selection: function() {

      if (this.selected_artifacts().length!=1 && this.opened_dialog!="background") return;

      if (this.opened_dialog=="background") {
        this.active_style[this.color_picker_target] = this.active_space.background_color;
      } else {
        if (!this.active_style[this.color_picker_target]) {
          this.active_style[this.color_picker_target] = this.default_style[this.color_picker_target];
        }
      }
      //console.log("active_style for "+this.color_picker_target+": ",this.active_style[this.color_picker_target]);

      var rgba = hex_to_rgba(this.active_style[this.color_picker_target]);

      var hsv = rgb_to_hsv(rgba.r, rgba.g, rgba.b);

      this.color_picker_hue = parseFloat(hsv.h*255);
      this.color_picker_saturation = parseFloat(hsv.s*255);
      this.color_picker_value = parseFloat(hsv.v*255);
      this.color_picker_opacity = parseInt(rgba.a);
      this.color_picker_rgb = rgb_to_hex(rgba.r,rgba.g,rgba.b);
    },

    update_selected_artifacts: function(change_func, override_locked) {
      var artifacts = this.selected_artifacts(!override_locked);

      if (!artifacts.length) return;

      this.update_artifacts(artifacts, change_func);
      this.update_selection_metrics();
    },

    nudge_selected_artifacts: function(dx, dy, event) {
      if (this.present_mode) {
        if (dx>0 || dy>0) {
          this.go_to_next_zone();
          return;
        }
        if (dx<0 || dy<0) {
          this.go_to_previous_zone();
          return;
        }
      }

      if (!this.selected_artifacts().length) {
        if (!$("#space").length) return;
        var el = $("#space")[0];
        el.scrollLeft+=dx*100;
        el.scrollTop +=dy*100;
        return;
      }

      if (this.active_space_is_readonly) return;

      if (event) {
        event.preventDefault();
        event.stopPropagation();
      }

      this.begin_transaction();

      this.update_selected_artifacts(function(a) {
        return {
          x: a.x+dx,
          y: a.y+dy
        };
      });
    },

    /* -------------------------------------------------------------------- */

    highest_z: function() {
      var z = _.max(this.active_space_artifacts.map(function(a){return a.z||0}));
      if (z<0) z=0;
      if (z>999) z=999;
      return z;
    },

    find_place_for_item: function(width, height) {
      var arts = this.active_space_artifacts;

      var tw = window.innerWidth;
      var th = window.innerHeight;
      var el = $("#space")[0];
      
      if (!el) return {x:0,y:0,z:1}; // FIXME

      var wrap = $(".wrapper");
      var wx = parseInt(wrap.css("margin-left"));
      var wy = parseInt(wrap.css("margin-top"));

      var x = parseInt((el.scrollLeft + tw/2)/this.viewport_zoom - width/2 - wx/this.viewport_zoom);
      var y = parseInt((el.scrollTop  + th/2)/this.viewport_zoom - height/2 - wy/this.viewport_zoom);

      /*
      if (this.opened_dialog!="none") {
        // we have less visible space if a dialog is obscuring sight
        y/=2;
      }
      */

      var z = this.highest_z()+1;

      if (arts.length==0) return {x:x,y:y};

      x += parseInt(Math.random()*20)-10;
      y += parseInt(Math.random()*20)-10;

      return {x:x,y:y,z:z};
    },

    save_audio_edit: function(a) { // just a helper to be called from view model
      this.opened_dialog = "none";
      this.update_board_artifact_viewmodel(a);
      save_artifact(a);
    },

    save_artifact: function(a, on_success) { // helper to be called from view model
      if (this.guest_nickname) {
        a.editor_name = this.guest_nickname;
      }

      this.update_board_artifact_viewmodel(a);
      save_artifact(a, on_success);
    },

    add_artifact: function (space, item_type, url, evt) {
      this.active_tool = "pointer";
      this.mouse_state = "idle";
      //this.hide_toolbar_artifacts();
      
      if (!url && (item_type == 'image' || item_type == 'video' || item_type == 'embed')) {
        url = prompt("URL?");
        if (!url || !url.length) return;
      }

      //this.opened_dialog = "none";

      var w=300,h=200;
      var z=this.highest_z()+1;

      // TODO: find solution for legacy types
      mimes = {
        "text": "text/html",
        "note": "text/html",
        "image": "image/jpg",
        "video": "video/mp4"
      };

      var new_item = {
        mime: mimes[item_type],
        description: "",
        payload_uri: url,
        payload_thumbnail_medium_uri: url || null,
        payload_thumbnail_web_uri: url || null,
        space_id: space._id,

        order: this.active_space_artifacts.length+1,
        valign: "middle",
        align: "center"
        //fill_color: "#f8f8f8"
      };

      if (mimes[item_type] == "text/html") {
        new_item.padding_left = 10;
        new_item.padding_top = 10;
        new_item.padding_right = 10;
        new_item.padding_bottom = 10;
        new_item.fill_color = "rgba(255,255,255,1)";
        new_item.description = "<p>Text</p>";
      }

      if (evt) {
        var point = this.cursor_point_to_space(evt);
        point.x-=100;
        point.y-=100;
      } else {
        var point = this.find_place_for_item(w,h);
        z = point.z;
      }

      new_item.x = parseInt(point.x);
      new_item.y = parseInt(point.y);
      new_item.z = z;
      new_item.w = w;
      new_item.h = h;

      if (this.guest_nickname) {
        new_item.editor_name = this.guest_nickname;
      }
      // console.log("new artifact", new_item);

      save_artifact(new_item, function(saved_item) {

        // console.log("saved artifact", saved_item);

        this.update_board_artifact_viewmodel(saved_item);
        this.active_space_artifacts.push(saved_item);

        if (!url) {
          this.select(null, saved_item);
        }

        if (item_type.match("text")) {
          this.editing_artifact_id = saved_item._id;
          window.setTimeout(function() {
            // FIXME: replace hack
            var el = $("#artifact-"+saved_item._id+" .text-editing");
            focus_contenteditable(el[0], false);
          },400);
        }
      }.bind(this));
    },

    go_to_first_zone: function() {
      this.discover_zones();
      if (!this.zones.length) return;
      this.zoom_to_zone(this.zones[0]);
    },

    go_to_previous_zone: function() {
      this.discover_zones();
      if (!this.zones.length) return;
      var prev_idx = (this.current_zone_idx-1);
      if (prev_idx<0) prev_idx = this.zones.length-1;
      this.current_zone_idx = prev_idx;
      this.zoom_to_zone(this.zones[this.current_zone_idx]);
    },

    go_to_next_zone: function() {
      this.discover_zones();
      if (!this.zones.length) return;
      var next_idx = ((this.current_zone_idx+1) % this.zones.length);
      this.current_zone_idx = next_idx;
      this.zoom_to_zone(this.zones[this.current_zone_idx]);
    },

    sort_zone_up: function(z) {
      var idx = this.zones.indexOf(z);
      if (idx<1) return;
      var new_zones = _.flatten([this.zones.slice(0,idx-1),[z],this.zones[idx-1],this.zones.slice(idx+1,this.zones.length)]);
      for (var i=0; i<new_zones.length; i++) {
        if (new_zones[i]) {
          if (!new_zones[i].style) new_zones[i].style = {};
          new_zones[i].order = i;
          save_artifact(new_zones[i]);
        }
      }
      this.discover_zones();
    },

    sort_zone_down: function(z) {
      var idx = this.zones.indexOf(z);
      if (idx>=this.zones.length) return;
      var new_zones = _.flatten([this.zones.slice(0,idx),this.zones[idx+1],[z],this.zones.slice(idx+2,this.zones.length)]);
      for (var i=0; i<new_zones.length; i++) {
        if (new_zones[i]) {
          if (!new_zones[i].style) new_zones[i].style = {};
          new_zones[i].order = i;
          save_artifact(new_zones[i]);
        }
      }
      this.discover_zones();
    },

    add_zone: function() {
      var w = 600;
      var h = 600;
      var point = this.find_place_for_item(w,h);

      var a = {
        space_id: this.active_space._id,
        mime: "x-spacedeck/zone",
        description: "Zone "+(this.zones.length+1),
        x: point.x,
        y: point.y,
        w: w,
        h: h,
        z: 0,
        valign: "middle",
        align: "center"
      };

      if (this.guest_nickname) {
        a.editor_name = this.guest_nickname;
      }

      save_artifact(a, function(saved_item) {
        this.update_board_artifact_viewmodel(saved_item);
        this.active_space_artifacts.push(saved_item);

        this.select(null, saved_item);
      }.bind(this));
    },

    add_shape: function(shape_type, evt) {
      var w = 200;
      var h = 200;

      if (shape_type=="cloud") {
        w = 400;
      }

      //var point = this.find_place_for_item(w,h);
      var point = this.cursor_point_to_space(evt);
      
      var a = {
        space_id: this.active_space._id,
        mime: "x-spacedeck/shape",
        description: "",
        x: point.x,
        y: point.y,
        z: point.z,
        w: w,
        h: h,
        stroke_color: "#ffffff",
        text_color: "#ffffff",
        stroke: 0,
        fill_color: "#000000",
        shape: shape_type,
        valign: "middle",
        align: "center",
      };

      if (this.guest_nickname) {
        a.editor_name = this.guest_nickname;
      }

      save_artifact(a, function(saved_item) {

        this.update_board_artifact_viewmodel(saved_item);
        this.active_space_artifacts.push(saved_item);

        this.select(null, saved_item);
      }.bind(this));
    },

    cursor_point_to_space: function(evt) {
      if (!evt) return {x:0,y:0};
      if (!$("#space").length) return {x:0,y:0};

      var el = $("#space")[0];
      var pt = parseInt($("#space").css("padding-top"));
      var ml = 0;
      var mt = 0;

      var px = evt.pageX;
      var py = evt.pageY;
      if (!("pageX" in evt) && ("originalEvent" in evt)) {
        px = evt.originalEvent.pageX;
        py = evt.originalEvent.pageY;
      }

      var ox = ((px + el.scrollLeft) - this.bounds_margin_horiz)/this.viewport_zoom;
      var oy = ((py + el.scrollTop) - pt - this.bounds_margin_vert)/this.viewport_zoom;
      return {x: ox, y: oy};
    },

    space_point_to_window: function(x,y) {
      var rx = 0;
      var ry = 0;
      
      var el = $("#space")[0];
      rx = x*this.viewport_zoom-el.scrollLeft+this.bounds_margin_horiz;
      ry = y*this.viewport_zoom-el.scrollTop+this.bounds_margin_vert;

      return {x:rx,y:ry};
    },

    create_artifact_via_upload: function(evt, file, multiple) {
      if (this.active_space_role=="viewer") {
        return false;
      }
            
      // 1. create placeholder artifact
      var w=300,h=150;
      var fill="transparent";
      if (file.type.match("audio")) {
        w=600;
        h=150;
        fill="#ffffff";
      }

      var point = this.cursor_point_to_space(evt);

      point.x-=w/2;
      point.y-=h/2;
      if (multiple) {
        point = this.find_place_for_item(w,h);
      }

      var a = {
        space_id: this.active_space._id,
        mime: file.type,
        description: "Uploading…",
        state: "uploading",
        payload_thumbnail_medium_uri: null,
        payload_thumbnail_web_uri: null,
        x: point.x,
        y: point.y,
        w: w,
        h: h,
        z: point.z,
        order: this.active_space_artifacts.length+1,
        fill_color: fill
      }

      this.update_board_artifact_viewmodel(a);

      // 2. post file
      if (this.guest_nickname) {
        a.editor_name = this.guest_nickname;
      }

      save_artifact(a, function(updated_a) {
        a = updated_a;
        this.update_board_artifact_viewmodel(a);
        this.active_space_artifacts.push(a);
        save_artifact_file(a, file, file.name, function(updated_a) {

          console.log("file saved. result: ",updated_a);

          // TODO: generify
          a.payload_uri = updated_a.payload_uri;
          a.payload_thumbnail_web_uri = updated_a.payload_thumbnail_web_uri;
          a.payload_thumbnail_medium_uri = updated_a.payload_thumbnail_medium_uri;
          a.payload_thumbnail_big_uri = updated_a.payload_thumbnail_big_uri;
          a.payload_alternatives = updated_a.payload_alternatives;
          a.mime = updated_a.mime;
          a.x = updated_a.x;
          a.y = updated_a.y;
          a.w = updated_a.w;
          a.h = updated_a.h;
          a.z = updated_a.z;
          a.state = updated_a.state;
          this.update_board_artifact_viewmodel(a);

        }.bind(this), null, function(e) {
          // upload progress
          var progress = e.loaded/e.total;

          if (progress=1) {
            a.description = "Converting Media…";
          } else {
            a.description = "Upload "+parseInt(progress*100)+"%";
          }
          this.update_board_artifact_viewmodel(a);
          a.view.progress = parseInt(progress*100);
        }.bind(this));
      }.bind(this), this.display_saving_error);
    },

    delete_selected_artifacts: function(evt, skip_deselect) {
      if (!this.active_space) return;

      if (evt) {
        evt.preventDefault();
        evt.stopPropagation();
      }

      this.begin_transaction();

      var ids = this.selected_artifacts().map(function(a){return a._id});
      var backup = [];
      var backup_ids = [];

      if (ids.length>1 && !skip_deselect) {
        if (!confirm("Delete "+ids.length+" items?")) return;
      }

      for (var i=0; i<ids.length; i++) {
        if (this.selected_artifacts_dict[ids[i]]) {
          var id = ids[i];
          var a = this.find_artifact_by_id(id);
          if (a) {
            backup.push(a);
            backup_ids.push(id);
            delete_artifact(a);
          }

          var idx = this.active_space_artifacts.indexOf(a);
          this.active_space_artifacts.splice(idx, 1);
        }
      }

      this.push_to_undo({
        action: "delete",
        artifact_ids: backup_ids,
        snapshot: backup
      });

      if (!skip_deselect) {
        this.deselect();
      }
    },

    find_artifact_by_id: function(id) {
      var ars = this.active_space_artifacts;
      for (var i=0; i<ars.length; i++) {
        var a = ars[i];
        if (a._id==id) return a;
      }
      return null;
    },

    selected_artifacts: function(exclude_locked) {
      if (!this.active_space || !this.active_space_artifacts) return [];
      return this.active_space_artifacts.filter(function(a) {
        var sel = this.artifact_is_selected(a);
        if (sel && a.locked) return !exclude_locked;
        return sel;
      }.bind(this));
    },

    delayed_edit_artifact: function(evt) {
      evt.stopPropagation();
      evt.preventDefault();

      var a = this.selected_artifacts()[0];

      var el = $("#ios-focuser-"+a._id);
      el.focus();
      el.select();

      this.toggle_selected_artifact_editing(true, true);
    },

    toggle_selected_artifact_editing: function(force_on, delayed) {
      var a = this.selected_artifacts()[0];

      if (!a) {
        this.editing_artifact_id = null;
        return;
      }

      if (this.editing_artifact_id == a._id && !force_on) {
        this.editing_artifact_id = null;
        return;
      }

      if (a.locked) return;

      if (!_.include(["text/html","x-spacedeck/shape","x-spacedeck/zone"],a.mime)) return;

      if (this.editing_artifact_id == a._id) return;

      this.editing_artifact_id = a._id;

      var ms = 100;
      if (delayed) ms = 500; // wait for slow devices

      window.setTimeout(function(){
        var el = $("#artifact-"+a._id+" .text-editing");
        if (el[0]) {
          // tuned weirdness for firefox, chrome + ios safari
          //el.select();
          focus_contenteditable(el[0], true);
        } else {
          // one more try (slow device)
          window.setTimeout(function(){
            var el = $("#artifact-"+a._id+" .text-editing");
            //el.select();
            focus_contenteditable(el[0], true);
          },ms);
        }
      },ms);
    },

    clear_formatting_walk: function(el,cmd,arg1,arg2) {
      if (el && el.style) {
        if (cmd == "preciseFontSize") {
          el.fontSize = null;
        } else if (cmd == "letterSpacing") {
          el.letterSpacing = null;
        } else if (cmd == "lineHeight") {
          el.lineHeight = null;
        } else if (cmd == "fontName") {
          el.fontFamily = null;
        } else if (cmd == "fontWeight") {
          el.fontWeight = null;
          el.fontStyle = null;
        } else if (cmd == "bold") {
          el.fontWeight = null;
        } else if (cmd == "italic") {
          el.fontStyle = null;
        } else if (cmd == "underline") {
          el.textDecoration = null;
        } else if (cmd == "strikeThrough") {
          el.textDecoration = null;
        } else if (cmd == "forecolor") {
          el.color = null;
        }
      }

      if (el && el.childNodes) {
        for (var i=0; i<el.childNodes.length; i++) {
          this.clear_formatting_walk(el.childNodes[i],cmd,arg1,arg2);
        }
      }
    },

    apply_formatting: function(evt,cmd,arg1,arg2) {
      if (evt) {
        evt.preventDefault();
        evt.stopPropagation();
      }

      if (this.skip_formatting) return;

      if (cmd=='createlink') {
        arg1 = prompt("Link URL?");
        if (!arg1) return;
      }

      var selected = this.selected_artifacts();
      var collapsed = false;

      if (!window.selection || window.selection.type == "Caret" || window.selection.type == "None") {
        collapsed = true;
      }

      // (selected.length==1 && (!this.editing_artifact_id))
      if (!this.editing_artifact_id || cmd == "preciseFontSize" || cmd == "forecolor") {
        for (var i=0; i<selected.length; i++) {
          var a = selected[i];
          var dom = $("<div>"+a.description+"</div>")[0];
          var el = dom.firstChild;

          do {
            // clear nested styles first
            if (el && el.childNodes) {
              for (var j=0; j<el.childNodes.length; j++) {
                this.clear_formatting_walk(el.childNodes[j],cmd,arg1,arg2);
              }
            }

            // toggle new style
            if (el && el.style) {
              if (cmd == "preciseFontSize") {
                if (arg1 == this.default_style.font_size+"px") {
                  el.style.fontSize = null;
                } else {
                  el.style.fontSize = arg1;
                }
              } else if (cmd == "letterSpacing") {
                if (arg1 == this.default_style.letter_spacing+"px") {
                  el.style.letterSpacing = null;
                } else {
                  el.style.letterSpacing = arg1;
                }
              } else if (cmd == "lineHeight") {
                if (arg1 == this.default_style.line_height+"em") {
                  el.style.lineHeight = null;
                } else {
                  el.style.lineHeight = arg1;
                }
              } else if (cmd == "fontName") {
                el.style.fontFamily = arg1;
              } else if (cmd == "fontWeight") {
                el.style.fontWeight = arg1;
                el.style.fontStyle = arg2;
              } else if (cmd == "bold") {
                el.style.fontWeight = (el.style.fontWeight!="bold"?"bold":"normal");
              } else if (cmd == "italic") {
                el.style.fontStyle = (el.style.fontStyle!="italic"?"italic":"normal");
              } else if (cmd == "underline") {
                el.style.textDecoration = (el.style.textDecoration!="underline"?"underline":"none");
              } else if (cmd == "strikeThrough") {
                el.style.textDecoration = (el.style.textDecoration!="line-through"?"line-through":"none");
              } else if (cmd == "forecolor") {
                el.style.color = arg1;
              }
            }
          } while (el && (el = el.nextSibling));

          if (a.description!=dom.innerHTML) {
            a.description = dom.innerHTML;

            console.log("new DOM:",dom.innerHTML);
            
            this.update_board_artifact_viewmodel(a);
            this.queue_artifact_for_save(a);

            if (this.editing_artifact_id) {
              var a = this.find_artifact_by_id(this.editing_artifact_id);
              var medium = this.medium_for_object[a._id];
              if (medium && a) {
                medium.value(a.description);
              }
            }
          }
        }

      } else if (this.editing_artifact_id) {
        // text level selection
        var a = this.find_artifact_by_id(this.editing_artifact_id);
        var medium = this.medium_for_object[a._id];
        
        if (medium && a) {
          medium.focus();
          medium.element.focus();
          medium.invokeElement(cmd);
          
          a.description = medium.value();
          this.queue_artifact_for_save(a);
        }
      }

      this.extract_text_format_from_selection();
    },

    remove_link_from_selected_artifacts: function() {
      this.update_selected_artifacts(function(a) {
        return {link_uri: ""};
      });
    },

    create_link_on_selected_artifacts: function() {
      var def = "";
      var sa = this.selected_artifacts();
      if (sa.length>=1) {
        if (sa[0].meta && sa[0].meta.link_uri) {
          def = sa[0].meta.link_uri;
        }
      }

      var insert_link_url = prompt("URL:",def);

      this.update_selected_artifacts(function(a) {
        var update = {link_uri: insert_link_url};

        if (a.payload_uri && a.payload_uri.match("webgrabber")) {
          var enc_uri = encodeURIComponent(btoa(insert_link_url));
          var thumb_uri = ENV.apiEndpoint + "/api/webgrabber/"+enc_uri;

          update.payload_uri = thumb_uri;
          update.payload_thumbnail_web_uri = thumb_uri;
          update.payload_thumbnail_medium_uri = thumb_uri;
          update.payload_thumbnail_big_uri = thumb_uri;
        }
        
        return update;
      });

      this.opened_dialog = "none";
    },

    clone_artifact: function(a,dx,dy,on_success) {
      var copy = _.cloneDeep(a);
      delete copy["$index"];
      delete copy["_id"];

      if (dx) copy.x += dx;
      if (dy) copy.y += dy;

      copy.order = this.active_space_artifacts.length+1;

      if (this.guest_nickname) {
        copy.editor_name = this.guest_nickname;
      }

      copy.space_id = this.active_space._id;

      save_artifact(copy, function(saved) {
        this.update_board_artifact_viewmodel(saved);
        this.active_space_artifacts.push(saved);
        if (on_success) {
          on_success(saved);
        } else {
          this.select(null,saved);
        }
      }.bind(this));
      return copy;
    },

    toggle_lock_of_selected_artifacts: function() {
      this.update_selected_artifacts(function(a) {
        return {locked: !a.locked};
      }, true);
    },

    duplicate_selected_artifacts: function() {
      var arts = this.selected_artifacts();
      for (var i=0; i<arts.length; i++) {
        var a = arts[i];
        var cloned = this.clone_artifact(a,50,50);
      }
    },

    copy_selected_artifacts_to_clipboard: function() {
      if ('ontouchstart' in window) return; // don't do this on touch devices

      $("#space-clipboard").focus();
      $("#space-clipboard").select();
    },

    handle_section_click: function(evt) {
      if (evt.target == evt.currentTarget) {
        this.deselect();
      }
    },

    handle_space_doubleclick: function(evt) {
      if (this.selected_artifacts().length) return;
      if (this.guest_nickname) return;
      if (this.active_space && this.active_space.access_mode == "public") return;

      //this.add_artifact(this.active_space, "text", null, evt);
    },

    handle_body_click: function(evt) {
      if (this.active_view == "space") {
        this.handle_section_click(evt);
      }
      this.close_dropdown(evt);
    },

    extract_text_format_from_selection: function() {
      if (!window.selection) return;

      var parents = $(window.selection.baseNode).parents().toArray();

      this.detected_text_formats = {};
      var formats = {
        "p":"Paragraph",
        "h1":"Headline 1",
        "h2":"Headline 2",
        "h3":"Headline 3",
        "h4":"Headline 4",
        "h5":"Headline 5",
        "h6":"Headline 6",
        "ul":"Bullet List",
        "ol":"Numbered List",
        "blockquote":"Blockquote"
      };

      for (var i=0; i<parents.length; i++) {
        var p = parents[i];
        if (p.contentEditable=="true") break;
        var nn = p.nodeName.toLowerCase();
        if (formats[nn]) {
          this.detected_text_formats[nn] = true;
          this.active_text_format_name = formats[nn];
        }
      }
    },

    // called on artifact keyup
    save_edited_artifact_text: function(evt) {

      // FIXME
      if (this.editing_artifact_id) {

        //var scribe = _scribe_handle_for_object[this.editing_artifact_id];
        var artifact = this.find_artifact_by_id(this.editing_artifact_id);
        if (artifact) {
          //artifact.description = scribe.getHTML();
          this.queue_artifact_for_save(artifact);
        }

        this.extract_text_format_from_selection();
      }

    },

    handle_section_paste: function(evt) {
      // TODO: very confusing
      return;
      
      if (this.editing_artifact_id) return;
      var pastedText = null;

      try {
        pastedText = evt.clipboardData.getData('text/plain');
      } catch (e) {
      }

      if (!pastedText) return;
      
      if (!pastedText.match(/<[a-zA-Z]+>/g)) {
        // crappy heuristic if this is actually HTML
        pastedText = pastedText.replace(/\n/g,"<br>");
      }
      
      this.insert_embedded_artifact(pastedText);
    },

    insert_embedded_artifact: function(text) {
      var space = this.active_space;
      if (!space) return;

      if (text[0]=='[' || text[0]=='{') {
        // might be json
        try {
          parsed = JSON.parse(text);
          if (text[0]=='{') parsed = [parsed];
          
          this.deselect();

          for (var i=0; i<parsed.length; i++) {
            if (parsed[i].mime) {
              var z = this.highest_z()+1;
              if (parsed.length==1) {
                var w = parsed[i].w;
                var h = parsed[i].h;
                var point = this.find_place_for_item(w,h);
                parsed[i].x = point.x;
                parsed[i].y = point.y;
                parsed[i].z = point.z;
              } else {
                parsed[i].x = parsed[i].x+50;
                parsed[i].y = parsed[i].y+50;
                parsed[i].y = parsed[i].z+z;
              }
              this.clone_artifact(parsed[i], 0,0, function(a) {
                this.multi_select([a]);
              }.bind(this));
            }
          }
          return;
        } catch(e) {
          // not json
        }
      }

      if (text.match(/^http[s]*\:\/\//)) {
        // crude url matching
        this.create_artifact_via_embed_url(text);
        return;
      }

      var new_item = {
        mime: "text/html",
        description: text.replace("\n", "<br />"),
        title: "",
        space_id: space._id
      };

      var w = 400;
      var h = 300;
      var point = this.find_place_for_item(w,h);

      new_item.x = point.x;
      new_item.y = point.y;
      new_item.w = w;
      new_item.h = h;
      new_item.z = point.z;

      if (this.guest_nickname) {
        new_item.editor_name = this.guest_nickname;
      }

      save_artifact(new_item, function(saved_item) {
        this.update_board_artifact_viewmodel(saved_item);
        this.active_space_artifacts.push(saved_item);
      }.bind(this));
    },

    create_artifact_via_embed_url: function(url) {
      this.close_modal();
      
      var point = this.find_place_for_item(200,200);
      var z = this.highest_z()+1;

      var a = {
        space_id: this.active_space._id,
        mime: "image/png",
        description: url,
        state: "uploading",
        x: point.x,
        y: point.y,
        w: 200,
        h: 200,
        z: z,
        order: this.active_space_artifacts.length
      }

      var metadata = parse_link(url)
      
      if (!metadata) {
        return;
      }

      if (metadata.type == "unknown") {
        var enc_uri = encodeURIComponent(btoa(url));

        a.meta = {
          link_uri: url
        }

        if (this.guest_nickname) {
          a.editor_name = this.guest_nickname;
        }

        // step 1: create placeholder
        save_artifact(a, function(saved_a) {
          this.update_board_artifact_viewmodel(saved_a);
          this.active_space_artifacts.push(saved_a);

          var thumb_uri = ENV.apiEndpoint + "/api/webgrabber/"+enc_uri;

          // step 2: push payload_uri for processing
          saved_a.state = "idle";
          saved_a.payload_uri = thumb_uri;

          saved_a.payload_thumbnail_web_uri = thumb_uri;
          saved_a.payload_thumbnail_medium_uri = thumb_uri;
          saved_a.payload_thumbnail_big_uri = thumb_uri;

          save_artifact(saved_a, function(saved_a2) {
            this.update_board_artifact_viewmodel(saved_a);
          }.bind(this));

        }.bind(this));

        return;
      }

      var w = metadata.thumbnail_width || 200;
      var h = metadata.thumbnail_height || 200;

      if (w<200) w = 200;
      if (h<200) h = 200;

      if (metadata.provider_name == "soundcloud") {
        w = 500;
        h = 150;
      }

      a = _.extend(a, {
        mime: "oembed/"+metadata.type+"-"+metadata.provider_name,
        description: metadata.url || url,
        //payload_uri: metadata.url || url,
        payload_thumbnail_medium_uri: metadata.thumbnail_url,
        payload_thumbnail_web_uri: metadata.thumbnail_url,
        state: "idle",
        title: metadata.title,
        link_uri: metadata.url || url,
        x: point.x - w/2,
        y: point.y - h/2,
        w: w,
        h: h
      });

      if (this.guest_nickname) {
        a.editor_name = this.guest_nickname;
      }

      save_artifact(a, function(saved_a) {
        this.update_board_artifact_viewmodel(saved_a);
        this.active_space_artifacts.push(saved_a);
      }.bind(this));
    },

    create_artifact_via_payload_url: function(type, url) {
      this.add_artifact(this.active_space, type, url, null);
    },

    handle_touch_select_background_image: function() {
      $('#background-uploader').click();
    },

    handle_insert_image_url: function(url) {
      if (!url || !url.length) {
        $("#image_file_upload").click(); // redirect to file upload
        return;
      }

      this.create_artifact_via_payload_url("image", url);
      this.insert_image_url = "";
      // this.opened_dialog = "none";
    },

    handle_insert_video_url: function(url) {

      if (!url.length) {
        $("#video_file_upload").click(); // redirect to file upload
        return;
      }

      var object = parse_link(url);

      if (object) {
        this.create_artifact_via_embed_url(url);
      } else {
        this.create_artifact_via_payload_url("video", url);
      }

      this.insert_video_url = "";
      //this.opened_dialog = "none";
    },

    handle_insert_audio_url: function(url) {

      if (!url.length) {
        $("#audio_file_upload").click(); // redirect to file upload
        return;
      }

      var object = parse_link(url);

      if (object) {
        this.create_artifact_via_embed_url(url);
      } else {
        this.create_artifact_via_payload_url("audio", url);
      }
      this.insert_audio_url = "";
    },

    handle_generic_file_upload: function(evt) {
      var files = evt.target.files;
      this.opened_dialog = "none";

      if (files && files.length) {
        console.log("file: ",files[0]);

        for (var i=0; i<files.length; i++) {
          var file = files[i];
          if (file.type === "application/pdf") {
            var point = {x: 100, y: 100}; //fixme, center upload?
            this.dropped_point = point;
            this.pending_pdf_file = file;
            this.activate_modal('pdfoptions');
          } else {
            this.create_artifact_via_upload(null, file, true);
          }
        }
      }
    },

    handle_image_file_upload: function(evt) {
      this.handle_generic_file_upload(evt);
    },
    handle_video_file_upload: function(evt) {
      this.handle_generic_file_upload(evt);
    },
    handle_audio_file_upload: function(evt) {
      this.handle_generic_file_upload(evt);
    },

    handle_section_background_upload: function(evt) {
      var f = evt.target.files[0];
      this.space_background_uploading = true;
      save_space_background_file(this.active_space, f, function(space) {
        this.active_space = space;
        this.space_background_uploading = false;
      }.bind(this));
    },

    remove_section_background: function() {
      this.active_space.background_uri = null;
      save_space(this.active_space);
    },

    show_toolbar_props: function() {
      if (this.selection_metrics.count==0) return;
      arts = this.selected_artifacts();
      for (var i=0;i<arts.length; i++) {
        if (arts[i].mime=="x-spacedeck/zone") return;
      }
      this.toolbar_props_in = true;
    },
    
    hide_toolbar_props: function() {
      // FIXME test
      //this.toolbar_props_in = false;
    },
    
    show_toolbar_artifacts: function(x,y) {
      this.toolbar_artifacts_in = true;
    },
    
    hide_toolbar_artifacts: function() {
      this.toolbar_artifacts_in = false;
    },

    start_adding_artifact: function(evt) {
      evt = fixup_touches(evt);
    },

    start_drawing_scribble: function(evt) {
      this.active_tool = "scribble";
      this.opened_dialog = "none";
    },

    start_drawing_arrow: function(evt) {
      this.active_tool = "arrow";
      this.opened_dialog = "none";
    },

    start_drawing_line: function(evt) {
      this.active_tool = "line";
      this.opened_dialog = "none";
    },

    adjust_bounds_zoom: function() {
      if (!this.active_space) return;

      this.bounds_zoom = this.viewport_zoom;

      var eff_w = this.active_space.width*this.viewport_zoom;
      var eff_h = this.active_space.height*this.viewport_zoom;

      if (window.innerWidth>eff_w) {
        // horizontal centering
        this.bounds_margin_horiz = (window.innerWidth-eff_w)/2;
      } else {
        this.bounds_margin_horiz = 0;
      }

      if (window.innerHeight-80>eff_h) {
        // horizontal centering
        this.bounds_margin_vert = (window.innerHeight-eff_h)/2-80;
      } else {
        this.bounds_margin_vert = 0;
      }
    },

    zoom_to_original: function() {
      var old_zoom = this.viewport_zoom;
      this.viewport_zoom = 1;
      this.viewport_zoom_percent = parseInt(this.viewport_zoom*100);
      this.adjust_bounds_zoom();
      this.zoom_adjust_scroll(this.viewport_zoom/old_zoom);
    },

    zoom_to_fit: function() {
      var er = this.enclosing_rect(this.active_space_artifacts);

      if (!er) return;

      var pad = 200;
      er.x1-=pad;
      er.y1-=pad-100;
      er.x2+=pad;
      er.y2+=pad+100;

      this.zoom_to_rect(er, 1);
    },

    zoom_to_zone: function(z) {
      if (!$("#space").length) return;

      var er = this.enclosing_rect([z]);

      var el = $("#space")[0];
      var cur_r = {
        x1: el.scrollLeft/this.viewport_zoom,
        y1: el.scrollTop/this.viewport_zoom,
        x2: (el.scrollLeft+window.innerWidth)/this.viewport_zoom,
        y2: (el.scrollTop+window.innerHeight)/this.viewport_zoom
      };
      
      var pad = 10;
      er.x1-=pad;
      er.y1-=pad;
      er.x2+=pad;
      er.y2+=pad;

      if (!this.animation_running) {
        this.animation_running = true;
        this.animate_zoom_to_rect(er, 200, cur_r);

        this.current_zone_idx = this.zones.indexOf(z);
      }
    },

    zoom_to_rect: function(er, max_zoom) {
      if (!$("#space").length) return;

      var el = $("#space")[0];
      var w = er.x2-er.x1;
      var h = er.y2-er.y1;

      if (w>h) {
        this.viewport_zoom = window.innerWidth/w;

        if (window.innerHeight < h*this.viewport_zoom) {
          this.viewport_zoom = window.innerHeight/h;
        }
      } else {
        this.viewport_zoom = window.innerHeight/h;

        if (window.innerWidth < w*this.viewport_zoom) {
          this.viewport_zoom = window.innerWidth/w;
        }
      }

      if (max_zoom) {
        if (this.viewport_zoom>max_zoom) this.viewport_zoom = max_zoom;
      }

      if (this.viewport_zoom<0.05) this.viewport_zoom = 0.05;

      this.viewport_zoom_percent = parseInt(this.viewport_zoom*100);
      this.adjust_bounds_zoom();

      if (!el) return;

      var animate = function() {
        el.scrollTop = (er.y1+(h/2))*this.viewport_zoom-window.innerHeight/2;
        el.scrollLeft = (er.x1+(w/2))*this.viewport_zoom-window.innerWidth/2;
        this.handle_scroll();
      }.bind(this);

      if ("requestAnimationFrame" in window) {
        window.requestAnimationFrame(animate);
      } else {
        animate();
      }
    },

    animate_zoom_to_rect: function(target_r, duration, cur_r, elapsed) {
      if (!$("#space").length) return;

      var el = $("#space")[0];
      var anim_res = 20;
      if (!elapsed) elapsed = 0;
      
      if (duration>elapsed) {
        window.setTimeout(function() {
          this.animate_zoom_to_rect(target_r, duration, cur_r, elapsed+anim_res);
        }.bind(this), anim_res);

        /*var dx = (el.scrollLeft-ncx)/anim_res;
        var dy = (el.scrollLeft-ncy)/anim_res;

        el.scrollLeft += dx;
        el.scrollTop += dy;*/

        // interpolate
        var dx1 = ((target_r.x1-cur_r.x1)/duration)*elapsed;
        var dx2 = ((target_r.x2-cur_r.x2)/duration)*elapsed;
        var dy1 = ((target_r.y1-cur_r.y1)/duration)*elapsed;
        var dy2 = ((target_r.y2-cur_r.y2)/duration)*elapsed;

        var step_r = {
          x1: cur_r.x1+dx1,
          x2: cur_r.x2+dx2,
          y1: cur_r.y1+dy1,
          y2: cur_r.y2+dy2
        };

        /*console.log("cur_r: ",cur_r);
        console.log("target_r: ",target_r);
        console.log("step_r: ",step_r);*/

        this.zoom_to_rect(step_r);
      } else {
        // done
        this.zoom_to_rect(target_r);
        this.animation_running = false;
      }
    },

    zoom_to_point: function(p,amount) {
      var el = $("#space")[0];
      
      var sx = el.scrollLeft/this.viewport_zoom;
      var sy = el.scrollTop/this.viewport_zoom;
      var ww = window.innerWidth/(this.viewport_zoom);
      var wh = window.innerHeight/(this.viewport_zoom);

      var oxx = (p.x-(sx+ww/2))*amount;
      var oyy = (p.y-(sy+wh/2))*amount;
      var ox = -oxx;
      var oy = -oyy;
      
      var r = {
        x1: p.x-(ww/2)*amount + ox,
        y1: p.y-(wh/2)*amount + oy,
        x2: p.x+(ww/2)*amount + ox,
        y2: p.y+(wh/2)*amount + oy
      };

      this.zoom_to_rect(r,2);
    },

    throttled_zoom_to_point: _.throttle(function(p,a){
      this.zoom_to_point(p,a);
    }, 50),

    zoom_to_cursor: function(evt,amount) {
      var point = this.cursor_point_to_space(evt);
      this.throttled_zoom_to_point.bind(this)(point,amount);
    },

    zoom_adjust_scroll: function(f) {
      var adjust_scroll = function() {
        if (!$("#space").length) return;
        if (!this.active_space || !this.active_space_loaded) return;
        
        var el = $("#space")[0];

        var eff_w = this.active_space.width*this.viewport_zoom;
        var eff_h = this.active_space.height*this.viewport_zoom;
        
        var sx = el.scrollLeft;
        var sy = el.scrollTop;

        var cx = window.innerWidth/2;
        var cy = window.innerHeight/2;

        var ncx = f*(sx+cx)-cx;
        var ncy = f*(sy+cy)-cy;

        if (eff_w<window.innerWidth) ncx = 0;

        if (eff_h<window.innerHeight) ncy = 0;

        el.scrollLeft = ncx;
        el.scrollTop = ncy;

        this.handle_scroll();
      };

      if ("requestAnimationFrame" in window) {
        window.requestAnimationFrame(adjust_scroll.bind(this));
      } else {
        adjust_scroll();
      }
    },

    zoom_in: function() {
      if (!this.viewport_zoom) this.viewport_zoom = 1;
      var old_zoom = this.viewport_zoom;

      this.viewport_zoom *= 1.5;
      if (this.viewport_zoom>=2) {
        this.viewport_zoom = 2;
      }
      this.viewport_zoom_percent = parseInt(this.viewport_zoom*100);
      this.adjust_bounds_zoom();

      this.zoom_adjust_scroll(this.viewport_zoom/old_zoom);
    },

    zoom_out: function() {
      if (!this.viewport_zoom) this.viewport_zoom = 1;
      var old_zoom = this.viewport_zoom;

      this.viewport_zoom /= 1.5;
      if (this.viewport_zoom<0.05) {
        this.viewport_zoom = 0.05;
      }
      this.viewport_zoom_percent = parseInt(this.viewport_zoom*100);
      this.adjust_bounds_zoom();

      this.zoom_adjust_scroll(this.viewport_zoom/old_zoom);
    },

    activate_pan_tool: function(evt) {
      if (evt) {
        evt.stopPropagation();
        evt.preventDefault();
      }
      this.active_tool = "pan";

      if (this.stop_pan_timeout) {
        window.clearTimeout(this.stop_pan_timeout);
      }
      // pan exit hack
      this.stop_pan_timeout = window.setTimeout(function () {
        if (this.active_tool == "pan") {
          this.active_tool = "pointer";
        }
      }.bind(this),500);
    },

    approve_pdf_upload: function(evt,approve_pdf_upload, mode){
      this.close_modal();
      
      if(mode == "classic"){
        this.create_artifact_via_upload(evt, this.pending_pdf_file, false);
      }

      if(mode == "grid") {
        this.global_spinner = true;
        save_pdf_file(this.active_space, this.dropped_point, this.pending_pdf_file, approve_pdf_upload,  function(createdArtifacts){

          this.global_spinner = false;

          _.each(createdArtifacts, function(new_artifact){
            this.update_board_artifact_viewmodel(new_artifact);
            this.active_space_artifacts.push(new_artifact)
          }.bind(this));

        }.bind(this), function(xhr) {
          this.global_spinner = false;
          alert("Error PDF ("+xhr.status+")");
        }.bind(this));
      }

    },

    handle_data_drop: function(evt) {
      if (this.active_space_role=="viewer") {
        return false;
      }

      var json = evt.dataTransfer.getData('application/json');
      var dest_section = this.active_space;
      var files = evt.dataTransfer.files;

      if (files && files.length) {
        for (var i=0; i<files.length; i++) {
          var file = files[i];
          if (file.type === "application/pdf") {
            var point = this.cursor_point_to_space(evt);
            this.dropped_point = point;
            this.pending_pdf_file = file;
            this.activate_modal('pdfoptions');

          } else {
            this.create_artifact_via_upload(evt, file, (files.length>1));
          }
        }

      } else {
        var json = evt.dataTransfer.getData('application/json');

        if (json) {
          var parsed = JSON.parse(json);
          delete parsed._id;
          parsed.space_id = this.active_space._id;

          var w = 300;
          var h = 200;
          if (parsed.board && parsed.w && parsed.h) {
            w = parsed.w;
            h = parsed.h;
          }

          var point = this.cursor_point_to_space(evt);
          point.x-=w/2;
          point.y-=h/2;

          parsed.board = {
            x: point.x,
            y: point.y,
            w: w,
            h: h,
            z: 20
          };

          if (this.guest_nickname) {
            parsed.editor_name = this.guest_nickname;
          }

          save_artifact(parsed, function(saved_a) {
            this.update_board_artifact_viewmodel(saved_a);
            this.active_space_artifacts.push(saved_a);
          }.bind(this));
          return;
        }

        var html = evt.dataTransfer.getData('text/html');
        
        if (html) {
          var rx = /src="([^"]+)"/g;
          var m = rx.exec(html);
          if (m) {
            this.add_artifact(this.active_space, "image", m[1], evt);
          }
        }
      }
    },

    clear_search_results: function() {
      this.image_search_results = [];
      this.audio_search_results = [];
      this.video_search_results = [];
    },

    download_selected_artifacts: function() {
      var arts = this.selected_artifacts();
      if (arts.length!=1) return;

      if (arts[0].payload_uri) {
        try {
          window.open(arts[0].payload_uri);
        } catch (e) {
          // could not open window
        }
      }
    }
  }
}
