<!--
Copyright 2009 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<Module>
  <ModulePrefs title="Wave Canvas" height="240">
    <Require feature="rpc"/>
    <Require feature="dynamic-height"/>
  </ModulePrefs>

  <Content type="html">
    <![CDATA[
      <style type="text/css">
        div.button {
          width: 24px;
          height: 24px;
          margin: 3px;
          background-position: center center;
          background-repeat: no-repeat;
        }
        div.button:hover {
          border: 1px solid #0000ff;
        }
        .unselected {
          border: 1px solid #ffffff;
        }
        .selected {
          background-color: #e0e0ff;
          border: 1px solid #0000ff;
        }
        .author-tip {
          color: white;
          padding: 2px;
          font-size: 12px;
          z-index: 100;
          filter: alpha(opacity=50);
          -moz-opacity: 0.8;
          opacity: 0.8;
        }
      </style>

      <script type="text/javascript"
          src="http://wave-api.appspot.com/public/wave.js"></script>
      <script type="text/javascript"
          src="hhttp://ichikawa-public.appspot.com/wave/canvas/json2.js">
      </script>
      <script type="text/javascript"
          src="http://ichikawa-public.appspot.com/wave/canvas/raphael.js">
      </script>
      <script type="text/javascript">
        
        if (!window.console) {
          console = {
            log: function() { },
            error: function() { },
            dir: function() { }
          };
        }
        
        // Basic functions
        
        function $(name) {
          return document.getElementById(name);
        }
        
        function observe(elem, name, handler) {
          elem.addEventListener(name, handler, false);
        }
        
        function escapeHTML(str) {
          return (str + "").replace(/&/, "&amp;").replace(/</, "&lt;").
            replace(/>/, "&gt;").replace(/"/, "&quot;"). // "
            replace(/'/, "&#039;");
        }
    
        function positionOf(event) {
          return {x: event.clientX, y: event.clientY};
        }
        
        function pplus(lhs, rhs) {
          return {x: lhs.x + rhs.x, y: lhs.y + rhs.y};
        }
        
        function pminus(lhs, rhs) {
          return {x: lhs.x - rhs.x, y: lhs.y - rhs.y};
        }
    
        function hashColor(str) {
          var hash = 0;
          for (var i = 0; i < str.length; ++i) {
            hash = (hash * 31 + str.charCodeAt(i)) % 0x1000000;
          }
          var group = hash % 3;
          hash = Math.floor(hash / 3);
          var values = [];
          for (var i = 0; i < 3; ++i) {
            values[(group + i) % 3] =
              (i == 0 ? 128 : 0) + hash % 128;
            hash = Math.floor(hash / 128);
          }
          return "rgb(" + values.join(",") + ")";
        }
    
        // Shape
        
        function Shape() { }
        
        Shape.strokeWidth = 4;
        Shape.defaultStrokeColor = "#000000";
        Shape.hoverStrokeColor = "#0000ff";
        Shape.typeToClass = {};
        
        Shape.create = function(canvas, spec, key) {
          var klass = Shape.typeToClass[spec.type];
          if (!klass) {
            console.error("unsupported shape: ", spec);
            return null;
          }
          var shape = new klass();
          shape.canvas_ = canvas;
          shape.key_ = key;
          if (!spec.ghost) {
            shape.update(spec);
          }
          return shape;
        };
        
        Shape.prototype.spec = function() { return this.spec_; };
    
        // Updates shape specification and redraw SVG object (obj_).
        Shape.prototype.update = function(spec) {
          var self = this;
          this.spec_ = spec;
          if (this.obj_) this.obj_.remove();
          if (this.canvas_.tipTarget() == this) {
            this.canvas_.setTipTarget(null);
          }
          this.obj_ = this.createObject(spec);
          if (spec.rotation) this.obj_.rotate(spec.rotation, true);
          this.updateColor();
          observe(this.obj_.node, "mousedown",
            function(e) { self.onShapeMouseDown(e); });
          observe(this.obj_.node, "mouseover",
            function(e) { self.onShapeMouseOver(e); });
          observe(this.obj_.node, "mouseout",
            function(e) { self.onShapeMouseOut(e); });
        };
        
        Shape.prototype.move = function(vec) {
          for (var i = 0; i < this.spec_.points.length; ++i) {
            this.spec_.points[i] = pplus(this.spec_.points[i], vec);
          }
          this.update(this.spec_);
        };
        
        Shape.prototype.rotate = function(degree) {
          if (!this.spec_.rotation) this.spec_.rotation = 0;
          this.spec_.rotation += degree;
          this.update(this.spec_);
        };
        
        Shape.prototype.remove = function() {
          if (this.obj_) this.obj_.remove();
          if (this.canvas_.tipTarget() == this) {
            this.canvas_.setTipTarget(null);
          }
          this.spec_ = null;
        };
        
        Shape.prototype.submit = function() {
          var state = wave.getState();
          var delta = {};
          if (this.spec_) {
            if (!this.key_) {
              this.key_ = "shape." + Math.random();
              this.canvas_.registerShape(this.key_, this);
            }
            if (!this.spec_.author) {
              this.spec_.author = this.canvas_.author();
            }
            delta[this.key_] = JSON.stringify(this.spec_);
          } else {
            this.canvas_.registerShape(this.key_, null);
            delta[this.key_] = null;
          }
          console.log("submitDelta");
          console.dir(delta);
          state.submitDelta(delta);
        };
        
        Shape.prototype.updateColor = function() {
          if (this.obj_) {
            var color;
            if (this.hover_ && !this.canvas_.isCreateMode()) {
              color = Shape.hoverStrokeColor;
            } else if (this.spec_ && this.spec_.author) {
              color = hashColor(this.spec_.author);
            } else {
              color = Shape.defaultStrokeColor;
            }
            this.obj_.attr("stroke", color);
          }
        };
        
        // TODO consider rotation
        Shape.prototype.getBBox = function(e) {
          return this.obj_.getBBox();
        };
    
        Shape.prototype.onShapeMouseDown = function(e) {
          var mode = this.canvas_.mode();
          if (mode == "move" || mode == "rotate") {
            this.canvas_.setActiveShape(this);
            this.origin_ = positionOf(e);
          } else if (mode == "delete") {
            this.remove();
            this.submit();
          } else if (mode == "debug") {
            console.dir(this);
          }
        }
        
        Shape.prototype.onShapeMouseOver = function(e) {
          this.hover_ = true;
          this.updateColor();
          if (this.spec_.author) {
            this.canvas_.setTipTarget(this);
          }
        };
        Shape.prototype.onShapeMouseOut = function(e) {
          this.hover_ = false;
          this.updateColor();
          if (this.canvas_.tipTarget() == this) {
            this.canvas_.setTipTarget(null);
          }
        };
        
        Shape.prototype.onCanvasMouseDown = function(e) {
          if (this.canvas_.isCreateMode()) {
            this.onCanvasMouseDownOnCreate(e);
          }
        };
    
        Shape.prototype.onCanvasMouseMove = function(e) {
          var mode = this.canvas_.mode();
          if (mode == "move") {
            this.move(pminus(positionOf(e), this.origin_));
            this.origin_ = positionOf(e);
          } else if (mode == "rotate") {
            this.rotate(positionOf(e).x - this.origin_.x);
            this.origin_ = positionOf(e);
          } else if (this.canvas_.isCreateMode()) {
            this.onCanvasMouseMoveOnCreate(e);
          }
        };
        
        Shape.prototype.onCanvasMouseUp = function(e) {
          var mode = this.canvas_.mode();
          if (mode == "move" || mode == "rotate") {
            this.submit();
            this.canvas_.setActiveShape(null);
          } else if (this.canvas_.isCreateMode()) {
            this.onCanvasMouseUpOnCreate(e);
          }
        };
        
        Shape.prototype.onCanvasMouseUpOnCreate = function(e) {
          if (!this.spec_) return;
          if (this.isValid()) {
            this.submit();
          } else {
            this.remove();
          }
          this.canvas_.setActiveShape(null);
        };
        
        Shape.prototype.toString = function() {
          return "Shape(" + (this.spec_ ? this.spec_.type : "") + ")";
        }
    
        // Line
        
        function Line() { }
        
        Line.prototype = new Shape();
        Shape.typeToClass.line = Line;
        
        Line.prototype.createObject = function(spec) {
          var obj = this.canvas_.paper().path({stroke: "#000000"});
          obj.attr("stroke-width", Shape.strokeWidth);
          obj.moveTo(spec.points[0].x, spec.points[0].y);
          obj.lineTo(spec.points[1].x, spec.points[1].y);
          return obj;
        };
        
        Line.prototype.onCanvasMouseDownOnCreate = function(e) {
          this.spec_ = {type: "line", points: [positionOf(e)]};
        };
    
        Line.prototype.onCanvasMouseMoveOnCreate = function(e) {
          if (!this.spec_) return;
          this.spec_.points[1] = positionOf(e);
          this.update(this.spec_);
        };
        
        Line.prototype.isValid = function() {
          return this.spec_ && this.spec_.points.length == 2;
        }
        
        // Path
        
        function Path() { }
        
        Path.prototype = new Shape();
        Shape.typeToClass.path = Path;
        
        Path.prototype.createObject = function(spec) {
          var obj = this.canvas_.paper().path({stroke: "#000000"});
          obj.attr("stroke-width", Shape.strokeWidth);
          obj.moveTo(spec.points[0].x, spec.points[0].y);
          for (var i = 1; i < spec.points.length; ++i) {
            obj.lineTo(spec.points[i].x, spec.points[i].y);
          }
          return obj;
        };
        
        Path.prototype.onCanvasMouseDownOnCreate = function(e) {
          this.spec_ = {type: "path", points: [positionOf(e)]};
        };
    
        Path.prototype.onCanvasMouseMoveOnCreate = function(e) {
          if (!this.spec_) return;
          this.spec_.points.push(positionOf(e));
          this.update(this.spec_);
        };
        
        Path.prototype.isValid = function() {
          return this.spec_ && this.spec_.points.length >= 2;
        }
        
        // Ellipse
        
        function Ellipse() { }
        
        Ellipse.prototype = new Shape();
        Shape.typeToClass.ellipse = Ellipse;
        
        Ellipse.prototype.createObject = function(spec) {
          var obj = this.canvas_.paper().ellipse(
            (spec.points[0].x + spec.points[1].x) / 2,
            (spec.points[0].y + spec.points[1].y) / 2,
            Math.abs(spec.points[0].x - spec.points[1].x) / 2,
            Math.abs(spec.points[0].y - spec.points[1].y) / 2);
          obj.attr("fill", "#ffa0a0");
          obj.attr("stroke", "#000000");
          obj.attr("stroke-width", Shape.strokeWidth);
          return obj;
        };
        
        Ellipse.prototype.onCanvasMouseDownOnCreate = function(e) {
          this.spec_ = {type: "ellipse", points: [positionOf(e)]};
        };
    
        Ellipse.prototype.onCanvasMouseMoveOnCreate = function(e) {
          if (!this.spec_) return;
          this.spec_.points[1] = positionOf(e);
          this.update(this.spec_);
        };
        
        Ellipse.prototype.isValid = function() {
          return this.spec_ && this.spec_.points.length == 2;
        }
        
        // Text
        
        function Text() { }
        
        Text.prototype = new Shape();
        Shape.typeToClass.text = Text;
        
        Text.prototype.createObject = function(spec) {
          var obj = this.canvas_.paper().text(
            spec.points[0].x, spec.points[0].y, spec.text);
          obj.attr("font-size", "20px");
          return obj;
        };
        
        Text.prototype.onCanvasMouseDownOnCreate = function(e) {
          var str = prompt("Input text:");
          if (!str) return;
          this.spec_ = {type: "text", points: [positionOf(e)], text: str};
          this.update(this.spec_);
          this.submit();
          this.canvas_.setActiveShape(null);
        };
    
        Text.prototype.onCanvasMouseMoveOnCreate = function(e) {
        };
        
        Text.prototype.isValid = function() {
          return this.spec_ && this.spec_.points.length == 1;
        }
        
        // Canvas
        
        function Canvas() {
          var self = this;
          this.shapes_ = {};
          this.iconWidth_ = 32;
          this.iconHeight_ = 32;
          this.fakeAuthor_ = null;
    
          this.setSize(document.body.clientWidth, 240);
          var buttons = $("toolbox").childNodes;
          var j = 0;
          for (var i = 0; i < buttons.length; ++i) {
            if (buttons[i].className != "button unselected") continue;
            buttons[i].style.position = "absolute";
            buttons[i].style.left =
              ((j % 2) * this.iconWidth_) + "px";
            buttons[i].style.top =
              (Math.floor(j / 2) * this.iconHeight_) + "px";
            ++j;
          }
    
          wave.setStateCallback(
            function(state){ self.onStateChanged(state); });
          observe(window, "mousedown", function(e){ self.onMouseDown(e) });
          observe(window, "mousemove", function(e){ self.onMouseMove(e) });
          observe(window, "mouseup", function(e){ self.onMouseUp(e) });
          observe(window, "resize", function(e){ self.onResize(e); });
    
          this.setMode("path");
        }
        
        Canvas.prototype.paper = function(){ return this.paper_; }
        Canvas.prototype.mode = function(){ return this.mode_; }
        Canvas.prototype.tipTarget = function(){ return this.tipTarget_; }
        
        Canvas.prototype.setMode = function(mode) {
          if (this.mode_) {
            $(this.mode_ + "_mode_button").className = "button unselected";
          }
          this.mode_ = mode;
          this.setActiveShape(null);
          $(this.mode_ + "_mode_button").className = "button selected";
          if (mode == "debug") {
            //this.dumpState();
          }
        };
        
        Canvas.prototype.setActiveShape = function(shape) {
          this.activeShape_ = shape;
          if (!this.activeShape_ && this.isCreateMode()) {
            this.activeShape_ =
              Shape.create(this, {type: this.mode_, ghost: true});
          }
        };
        
        Canvas.prototype.registerShape = function(key, shape) {
          this.shapes_[key] = shape;
        };
        
        Canvas.prototype.isCreateMode = function() {
          return this.mode_ in Shape.typeToClass;
        };
        
        Canvas.prototype.clearAndSubmit = function() {
          var state = wave.getState();
          var keys = state.getKeys();
          var delta = {};
          for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            if (key.match(/^shape\./)) {
              delta[key] = null;
            }
          }
          state.submitDelta(delta);
        };
        
        Canvas.prototype.dumpState = function() {
          var state = wave.getState();
          var keys = state.getKeys();
          for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            var value = state.get(key);
            console.log([key, value]);
          }
        };
    
        Canvas.prototype.author = function() {
          if (this.fakeAuthor_) return this.fakeAuthor_;
          var viewer = wave.getViewer();
          return viewer && viewer.getId();
        };
    
        Canvas.prototype.setTipTarget = function(target) {
          if (target) {
            var box = target.getBBox();
            var author = target.spec().author;
            var authorObj = wave.getParticipantById(author);
            var label = authorObj ? authorObj.getDisplayName() : author;
            $("author-tip").style.left = (box.x + box.width + 3) + "px";
            $("author-tip").style.top = (box.y + box.height + 3) + "px";
            $("author-tip").style.backgroundColor = hashColor(author);
            $("author-tip").innerHTML = escapeHTML(label);
            $("author-tip").style.display = "";
          } else {
            $("author-tip").style.display = "none";
          }
          this.tipTarget_ = target;
        };
    
        Canvas.prototype.setSize = function(width, height) {
          var heightChanged = height != this.height_;
          this.width_ = width;
          this.height_ = height;
          // placeholder is used to let _IG_AdjustIFrameHeight(); know
          // the correct size.
          $("placeholder").style.width = width + "px";
          $("placeholder").style.height = height + "px";
          if (this.paper_) {
            this.paper_.setSize(width, height);
          } else {
            this.paper_ = Raphael(0, 0, width, height);
          }
          if (this.background_) this.background_.remove();
          this.background_ = this.paper_.rect(0, 0, width, height);
          this.background_.attr("fill", "#ffffe0");
          this.background_.attr("stroke", "#808080");
          this.background_.toBack();
          if (heightChanged) _IG_AdjustIFrameHeight();
        }
        
        Canvas.prototype.onStateChanged = function(state) {
          console.log("onStateChanged");
          for (var key in this.shapes_) {
            if (this.shapes_[key]) this.shapes_[key].remove();
          }
          this.shapes_ = {};
          var keys = state.getKeys();
          for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            var value = state.get(key);
            if (key.match(/^shape\./)) {
              this.shapes_[key] = Shape.create(this, eval("(" + value + ")"), key);
            }
          }
          if (state.get("canvas.height") &&
              parseInt(state.get("canvas.height")) != this.height_) {
            this.setSize(this.width_, parseInt(state.get("canvas.height")));
          }
        };
        
        Canvas.prototype.onMouseDown = function(e) {
          if (!this.isOnCanvas(e)) return;
          if (this.activeShape_) this.activeShape_.onCanvasMouseDown(e);
        };
        
        Canvas.prototype.onMouseMove = function(e) {
          if (!this.isOnCanvas(e)) return;
          if (this.activeShape_) this.activeShape_.onCanvasMouseMove(e);
        };
        
        Canvas.prototype.onMouseUp = function(e) {
          if (this.activeShape_) this.activeShape_.onCanvasMouseUp(e);
        };
        
        Canvas.prototype.onResize = function(e) {
          this.setSize(document.body.clientWidth, this.height_);
        };
        
        Canvas.prototype.onCanvasHeightButtonClicked = function() {
          var height = prompt("New canvas height:", this.height_);
          if (height < this.iconHeight_ * 5) height = this.iconHeight_ * 5;
          if (height) {
            wave.getState().submitDelta({"canvas.height": height});
          }
        };
        
        Canvas.prototype.isOnCanvas = function(e) {
          var elem = e.target;
          while (elem) {
            if (elem == $("toolbox")) return false;
            elem = elem.parentNode;
          }
          return e.clientX >= 0 && e.clientY >= 0 &&
            e.clientX <= this.width_ && e.clientY <= this.height_;
        };
        
        var canvas = null;
        
        function main() {
          if (!wave || !wave.isInWaveContainer()) {
            console.error("wave is required");
            return;
          }
          console.log("init");
          canvas = new Canvas();
        }
    
        gadgets.util.registerOnLoadHandler(main);
    
      </script>
      <div id="placeholder"></div>
      <div id="toolbox"
          style="position: absolute; left: 0px; top: 0px; width: 64px; z-index: 10;">
        <div id="path_mode_button"
          class="button unselected"
          onclick="canvas.setMode('path');"
          title="Draw freehand"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/freehand.png);">
        </div>
        <div id="line_mode_button"
          class="button unselected"
          onclick="canvas.setMode('line');"
          title="Draw line"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/line.png);">
        </div>
        <div id="ellipse_mode_button"
          class="button unselected"
          onclick="canvas.setMode('ellipse');"
          title="Draw ellipse"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/circle.png);">
        </div>
        <div id="text_mode_button"
          class="button unselected"
          onclick="canvas.setMode('text');"
          title="Draw text"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/text.png);">
        </div>
        <div id="move_mode_button"
          class="button unselected"
          onclick="canvas.setMode('move');"
          title="Move shape"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/select.png);">
        </div>
        <div id="rotate_mode_button"
          class="button unselected"
          onclick="canvas.setMode('rotate');"
          title="Rotate shape"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/rotate.png);">
        </div>
        <div id="delete_mode_button"
          class="button unselected"
          onclick="canvas.setMode('delete');"
          title="Delete shape"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/delete.png);">
        </div>
        <div id="canvas_height_button"
          class="button unselected"
          onclick="canvas.onCanvasHeightButtonClicked();"
          title="Change canvas height"
          style="background-image: url(http://ichikawa-public.appspot.com/wave/canvas/images/canvas_height.png);">
        </div>
        <div id="debug_mode_button"
          class="button unselected"
          onclick="canvas.setMode('debug');"
          title="Debug">
        </div>
      </div>
      <span id="author-tip"
        class="author-tip" style="position: absolute; display: none;">
      </span>
    ]]>
  </Content>
</Module>

