// Copyright 2006 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.

// Known Issues:
//
// * Patterns only support repeat.
// * Radial gradient are not implemented. The VML version of these look very
//   different from the canvas one.
// * Clipping paths are not implemented.
// * Coordsize. The width and height attribute have higher priority than the
//   width and height style values which isn't correct.
// * Painting mode isn't implemented.
// * Canvas width/height should is using content-box by default. IE in
//   Quirks mode will draw the canvas using border-box. Either change your
//   doctype to HTML5
//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
//   or use Box Sizing Behavior from WebFX
//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
// * Non uniform scaling does not correctly scale strokes.
// * Filling very large shapes (above 5000 points) is buggy.
// * Optimize. There is always room for speed improvements.

// Only add this code if we do not already have a canvas implementation
(function () {
  if (!document.createElement("canvas").getContext) {
    // alias some functions to make (compiled) code shorter
    var m = Math;
    var mr = m.round;
    var ms = m.sin;
    var mc = m.cos;
    var abs = m.abs;
    var sqrt = m.sqrt;

    // this is used for sub pixel precision
    var Z = 10;
    var Z2 = Z / 2;

    var IE_VERSION = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];

    /**
     * This funtion is assigned to the <canvas> elements as element.getContext().
     * @this {HTMLElement}
     * @return {CanvasRenderingContext2D_}
     */
    function getContext() {
      return (
        this.context_ || (this.context_ = new CanvasRenderingContext2D_(this))
      );
    }

    var slice = Array.prototype.slice;

    /**
     * Binds a function to an object. The returned function will always use the
     * passed in {@code obj} as {@code this}.
     *
     * Example:
     *
     *   g = bind(f, obj, a, b)
     *   g(c, d) // will do f.call(obj, a, b, c, d)
     *
     * @param {Function} f The function to bind the object to
     * @param {Object} obj The object that should act as this when the function
     *     is called
     * @param {*} var_args Rest arguments that will be used as the initial
     *     arguments when the function is called
     * @return {Function} A new function that has bound this
     */
    function bind(f, obj, var_args) {
      var a = slice.call(arguments, 2);
      return function () {
        return f.apply(obj, a.concat(slice.call(arguments)));
      };
    }

    function encodeHtmlAttribute(s) {
      return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
    }

    function addNamespace(doc, prefix, urn) {
      if (!doc.namespaces[prefix]) {
        doc.namespaces.add(prefix, urn, "#default#VML");
      }
    }

    function addNamespacesAndStylesheet(doc) {
      addNamespace(doc, "g_vml_", "urn:schemas-microsoft-com:vml");
      addNamespace(doc, "g_o_", "urn:schemas-microsoft-com:office:office");

      // Setup default CSS.  Only add one style sheet per document
      if (!doc.styleSheets["ex_canvas_"]) {
        var ss = doc.createStyleSheet();
        ss.owningElement.id = "ex_canvas_";
        ss.cssText =
          "canvas{display:inline-block;overflow:hidden;" +
          // default size is 300x150 in Gecko and Opera
          "text-align:left;width:300px;height:150px}";
      }
    }

    // Add namespaces and stylesheet at startup.
    addNamespacesAndStylesheet(document);

    var G_vmlCanvasManager_ = {
      init: function (opt_doc) {
        var doc = opt_doc || document;
        // Create a dummy element so that IE will allow canvas elements to be
        // recognized.
        doc.createElement("canvas");
        doc.attachEvent("onreadystatechange", bind(this.init_, this, doc));
      },

      init_: function (doc) {
        // find all canvas elements
        var els = doc.getElementsByTagName("canvas");
        for (var i = 0; i < els.length; i++) {
          this.initElement(els[i]);
        }
      },

      /**
       * Public initializes a canvas element so that it can be used as canvas
       * element from now on. This is called automatically before the page is
       * loaded but if you are creating elements using createElement you need to
       * make sure this is called on the element.
       * @param {HTMLElement} el The canvas element to initialize.
       * @return {HTMLElement} the element that was created.
       */
      initElement: function (el) {
        if (!el.getContext) {
          el.getContext = getContext;

          // Add namespaces and stylesheet to document of the element.
          addNamespacesAndStylesheet(el.ownerDocument);

          // Remove fallback content. There is no way to hide text nodes so we
          // just remove all childNodes. We could hide all elements and remove
          // text nodes but who really cares about the fallback content.
          el.innerHTML = "";

          // do not use inline function because that will leak memory
          el.attachEvent("onpropertychange", onPropertyChange);
          el.attachEvent("onresize", onResize);

          var attrs = el.attributes;
          if (attrs.width && attrs.width.specified) {
            // TODO: use runtimeStyle and coordsize
            // el.getContext().setWidth_(attrs.width.nodeValue);
            el.style.width = attrs.width.nodeValue + "px";
          } else {
            el.width = el.clientWidth;
          }
          if (attrs.height && attrs.height.specified) {
            // TODO: use runtimeStyle and coordsize
            // el.getContext().setHeight_(attrs.height.nodeValue);
            el.style.height = attrs.height.nodeValue + "px";
          } else {
            el.height = el.clientHeight;
          }
          //el.getContext().setCoordsize_()
        }
        return el;
      },
    };

    function onPropertyChange(e) {
      var el = e.srcElement;

      switch (e.propertyName) {
        case "width":
          el.getContext().clearRect();
          el.style.width = el.attributes.width.nodeValue + "px";
          // In IE8 this does not trigger onresize.
          el.firstChild.style.width = el.clientWidth + "px";
          break;
        case "height":
          el.getContext().clearRect();
          el.style.height = el.attributes.height.nodeValue + "px";
          el.firstChild.style.height = el.clientHeight + "px";
          break;
      }
    }

    function onResize(e) {
      var el = e.srcElement;
      if (el.firstChild) {
        el.firstChild.style.width = el.clientWidth + "px";
        el.firstChild.style.height = el.clientHeight + "px";
      }
    }

    G_vmlCanvasManager_.init();

    // precompute "00" to "FF"
    var decToHex = [];
    for (var i = 0; i < 16; i++) {
      for (var j = 0; j < 16; j++) {
        decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
      }
    }

    function createMatrixIdentity() {
      return [
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1],
      ];
    }

    function matrixMultiply(m1, m2) {
      var result = createMatrixIdentity();

      for (var x = 0; x < 3; x++) {
        for (var y = 0; y < 3; y++) {
          var sum = 0;

          for (var z = 0; z < 3; z++) {
            sum += m1[x][z] * m2[z][y];
          }

          result[x][y] = sum;
        }
      }
      return result;
    }

    function copyState(o1, o2) {
      o2.fillStyle = o1.fillStyle;
      o2.lineCap = o1.lineCap;
      o2.lineJoin = o1.lineJoin;
      o2.lineWidth = o1.lineWidth;
      o2.miterLimit = o1.miterLimit;
      o2.shadowBlur = o1.shadowBlur;
      o2.shadowColor = o1.shadowColor;
      o2.shadowOffsetX = o1.shadowOffsetX;
      o2.shadowOffsetY = o1.shadowOffsetY;
      o2.strokeStyle = o1.strokeStyle;
      o2.globalAlpha = o1.globalAlpha;
      o2.font = o1.font;
      o2.textAlign = o1.textAlign;
      o2.textBaseline = o1.textBaseline;
      o2.arcScaleX_ = o1.arcScaleX_;
      o2.arcScaleY_ = o1.arcScaleY_;
      o2.lineScale_ = o1.lineScale_;
    }

    var colorData = {
      aliceblue: "#F0F8FF",
      antiquewhite: "#FAEBD7",
      aquamarine: "#7FFFD4",
      azure: "#F0FFFF",
      beige: "#F5F5DC",
      bisque: "#FFE4C4",
      black: "#000000",
      blanchedalmond: "#FFEBCD",
      blueviolet: "#8A2BE2",
      brown: "#A52A2A",
      burlywood: "#DEB887",
      cadetblue: "#5F9EA0",
      chartreuse: "#7FFF00",
      chocolate: "#D2691E",
      coral: "#FF7F50",
      cornflowerblue: "#6495ED",
      cornsilk: "#FFF8DC",
      crimson: "#DC143C",
      cyan: "#00FFFF",
      darkblue: "#00008B",
      darkcyan: "#008B8B",
      darkgoldenrod: "#B8860B",
      darkgray: "#A9A9A9",
      darkgreen: "#006400",
      darkgrey: "#A9A9A9",
      darkkhaki: "#BDB76B",
      darkmagenta: "#8B008B",
      darkolivegreen: "#556B2F",
      darkorange: "#FF8C00",
      darkorchid: "#9932CC",
      darkred: "#8B0000",
      darksalmon: "#E9967A",
      darkseagreen: "#8FBC8F",
      darkslateblue: "#483D8B",
      darkslategray: "#2F4F4F",
      darkslategrey: "#2F4F4F",
      darkturquoise: "#00CED1",
      darkviolet: "#9400D3",
      deeppink: "#FF1493",
      deepskyblue: "#00BFFF",
      dimgray: "#696969",
      dimgrey: "#696969",
      dodgerblue: "#1E90FF",
      firebrick: "#B22222",
      floralwhite: "#FFFAF0",
      forestgreen: "#228B22",
      gainsboro: "#DCDCDC",
      ghostwhite: "#F8F8FF",
      gold: "#FFD700",
      goldenrod: "#DAA520",
      grey: "#808080",
      greenyellow: "#ADFF2F",
      honeydew: "#F0FFF0",
      hotpink: "#FF69B4",
      indianred: "#CD5C5C",
      indigo: "#4B0082",
      ivory: "#FFFFF0",
      khaki: "#F0E68C",
      lavender: "#E6E6FA",
      lavenderblush: "#FFF0F5",
      lawngreen: "#7CFC00",
      lemonchiffon: "#FFFACD",
      lightblue: "#ADD8E6",
      lightcoral: "#F08080",
      lightcyan: "#E0FFFF",
      lightgoldenrodyellow: "#FAFAD2",
      lightgreen: "#90EE90",
      lightgrey: "#D3D3D3",
      lightpink: "#FFB6C1",
      lightsalmon: "#FFA07A",
      lightseagreen: "#20B2AA",
      lightskyblue: "#87CEFA",
      lightslategray: "#778899",
      lightslategrey: "#778899",
      lightsteelblue: "#B0C4DE",
      lightyellow: "#FFFFE0",
      limegreen: "#32CD32",
      linen: "#FAF0E6",
      magenta: "#FF00FF",
      mediumaquamarine: "#66CDAA",
      mediumblue: "#0000CD",
      mediumorchid: "#BA55D3",
      mediumpurple: "#9370DB",
      mediumseagreen: "#3CB371",
      mediumslateblue: "#7B68EE",
      mediumspringgreen: "#00FA9A",
      mediumturquoise: "#48D1CC",
      mediumvioletred: "#C71585",
      midnightblue: "#191970",
      mintcream: "#F5FFFA",
      mistyrose: "#FFE4E1",
      moccasin: "#FFE4B5",
      navajowhite: "#FFDEAD",
      oldlace: "#FDF5E6",
      olivedrab: "#6B8E23",
      orange: "#FFA500",
      orangered: "#FF4500",
      orchid: "#DA70D6",
      palegoldenrod: "#EEE8AA",
      palegreen: "#98FB98",
      paleturquoise: "#AFEEEE",
      palevioletred: "#DB7093",
      papayawhip: "#FFEFD5",
      peachpuff: "#FFDAB9",
      peru: "#CD853F",
      pink: "#FFC0CB",
      plum: "#DDA0DD",
      powderblue: "#B0E0E6",
      rosybrown: "#BC8F8F",
      royalblue: "#4169E1",
      saddlebrown: "#8B4513",
      salmon: "#FA8072",
      sandybrown: "#F4A460",
      seagreen: "#2E8B57",
      seashell: "#FFF5EE",
      sienna: "#A0522D",
      skyblue: "#87CEEB",
      slateblue: "#6A5ACD",
      slategray: "#708090",
      slategrey: "#708090",
      snow: "#FFFAFA",
      springgreen: "#00FF7F",
      steelblue: "#4682B4",
      tan: "#D2B48C",
      thistle: "#D8BFD8",
      tomato: "#FF6347",
      turquoise: "#40E0D0",
      violet: "#EE82EE",
      wheat: "#F5DEB3",
      whitesmoke: "#F5F5F5",
      yellowgreen: "#9ACD32",
    };

    function getRgbHslContent(styleString) {
      var start = styleString.indexOf("(", 3);
      var end = styleString.indexOf(")", start + 1);
      var parts = styleString.substring(start + 1, end).split(",");
      // add alpha if needed
      if (parts.length != 4 || styleString.charAt(3) != "a") {
        parts[3] = 1;
      }
      return parts;
    }

    function percent(s) {
      return parseFloat(s) / 100;
    }

    function clamp(v, min, max) {
      return Math.min(max, Math.max(min, v));
    }

    function hslToRgb(parts) {
      var r, g, b, h, s, l;
      h = (parseFloat(parts[0]) / 360) % 360;
      if (h < 0) h++;
      s = clamp(percent(parts[1]), 0, 1);
      l = clamp(percent(parts[2]), 0, 1);
      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hueToRgb(p, q, h + 1 / 3);
        g = hueToRgb(p, q, h);
        b = hueToRgb(p, q, h - 1 / 3);
      }

      return (
        "#" +
        decToHex[Math.floor(r * 255)] +
        decToHex[Math.floor(g * 255)] +
        decToHex[Math.floor(b * 255)]
      );
    }

    function hueToRgb(m1, m2, h) {
      if (h < 0) h++;
      if (h > 1) h--;

      if (6 * h < 1) return m1 + (m2 - m1) * 6 * h;
      else if (2 * h < 1) return m2;
      else if (3 * h < 2) return m1 + (m2 - m1) * (2 / 3 - h) * 6;
      else return m1;
    }

    var processStyleCache = {};

    function processStyle(styleString) {
      if (styleString in processStyleCache) {
        return processStyleCache[styleString];
      }

      var str,
        alpha = 1;

      styleString = String(styleString);
      if (styleString.charAt(0) == "#") {
        str = styleString;
      } else if (/^rgb/.test(styleString)) {
        var parts = getRgbHslContent(styleString);
        var str = "#",
          n;
        for (var i = 0; i < 3; i++) {
          if (parts[i].indexOf("%") != -1) {
            n = Math.floor(percent(parts[i]) * 255);
          } else {
            n = +parts[i];
          }
          str += decToHex[clamp(n, 0, 255)];
        }
        alpha = +parts[3];
      } else if (/^hsl/.test(styleString)) {
        var parts = getRgbHslContent(styleString);
        str = hslToRgb(parts);
        alpha = parts[3];
      } else {
        str = colorData[styleString] || styleString;
      }
      return (processStyleCache[styleString] = { color: str, alpha: alpha });
    }

    var DEFAULT_STYLE = {
      style: "normal",
      variant: "normal",
      weight: "normal",
      size: 10,
      family: "sans-serif",
    };

    // Internal text style cache
    var fontStyleCache = {};

    function processFontStyle(styleString) {
      if (fontStyleCache[styleString]) {
        return fontStyleCache[styleString];
      }

      var el = document.createElement("div");
      var style = el.style;
      try {
        style.font = styleString;
      } catch (ex) {
        // Ignore failures to set to invalid font.
      }

      return (fontStyleCache[styleString] = {
        style: style.fontStyle || DEFAULT_STYLE.style,
        variant: style.fontVariant || DEFAULT_STYLE.variant,
        weight: style.fontWeight || DEFAULT_STYLE.weight,
        size: style.fontSize || DEFAULT_STYLE.size,
        family: style.fontFamily || DEFAULT_STYLE.family,
      });
    }

    function getComputedStyle(style, element) {
      var computedStyle = {};

      for (var p in style) {
        computedStyle[p] = style[p];
      }

      // Compute the size
      var canvasFontSize = parseFloat(element.currentStyle.fontSize),
        fontSize = parseFloat(style.size);

      if (typeof style.size == "number") {
        computedStyle.size = style.size;
      } else if (style.size.indexOf("px") != -1) {
        computedStyle.size = fontSize;
      } else if (style.size.indexOf("em") != -1) {
        computedStyle.size = canvasFontSize * fontSize;
      } else if (style.size.indexOf("%") != -1) {
        computedStyle.size = (canvasFontSize / 100) * fontSize;
      } else if (style.size.indexOf("pt") != -1) {
        computedStyle.size = fontSize / 0.75;
      } else {
        computedStyle.size = canvasFontSize;
      }

      // Different scaling between normal text and VML text. This was found using
      // trial and error to get the same size as non VML text.
      computedStyle.size *= 0.981;

      return computedStyle;
    }

    function buildStyle(style) {
      return (
        style.style +
        " " +
        style.variant +
        " " +
        style.weight +
        " " +
        style.size +
        "px " +
        style.family
      );
    }

    var lineCapMap = {
      butt: "flat",
      round: "round",
    };

    function processLineCap(lineCap) {
      return lineCapMap[lineCap] || "square";
    }

    /**
     * This class implements CanvasRenderingContext2D interface as described by
     * the WHATWG.
     * @param {HTMLElement} canvasElement The element that the 2D context should
     * be associated with
     */
    function CanvasRenderingContext2D_(canvasElement) {
      this.m_ = createMatrixIdentity();

      this.mStack_ = [];
      this.aStack_ = [];
      this.currentPath_ = [];

      // Canvas context properties
      this.strokeStyle = "#000";
      this.fillStyle = "#000";

      this.lineWidth = 1;
      this.lineJoin = "miter";
      this.lineCap = "butt";
      this.miterLimit = Z * 1;
      this.globalAlpha = 1;
      this.font = "10px sans-serif";
      this.textAlign = "left";
      this.textBaseline = "alphabetic";
      this.canvas = canvasElement;

      var cssText =
        "width:" +
        canvasElement.clientWidth +
        "px;height:" +
        canvasElement.clientHeight +
        "px;overflow:hidden;position:absolute";
      var el = canvasElement.ownerDocument.createElement("div");
      el.style.cssText = cssText;
      canvasElement.appendChild(el);

      var overlayEl = el.cloneNode(false);
      // Use a non transparent background.
      overlayEl.style.backgroundColor = "red";
      overlayEl.style.filter = "alpha(opacity=0)";
      canvasElement.appendChild(overlayEl);

      this.element_ = el;
      this.arcScaleX_ = 1;
      this.arcScaleY_ = 1;
      this.lineScale_ = 1;
    }

    var contextPrototype = CanvasRenderingContext2D_.prototype;
    contextPrototype.clearRect = function () {
      if (this.textMeasureEl_) {
        this.textMeasureEl_.removeNode(true);
        this.textMeasureEl_ = null;
      }
      this.element_.innerHTML = "";
    };

    contextPrototype.beginPath = function () {
      // TODO: Branch current matrix so that save/restore has no effect
      //       as per safari docs.
      this.currentPath_ = [];
    };

    contextPrototype.moveTo = function (aX, aY) {
      var p = getCoords(this, aX, aY);
      this.currentPath_.push({ type: "moveTo", x: p.x, y: p.y });
      this.currentX_ = p.x;
      this.currentY_ = p.y;
    };

    contextPrototype.lineTo = function (aX, aY) {
      var p = getCoords(this, aX, aY);
      this.currentPath_.push({ type: "lineTo", x: p.x, y: p.y });

      this.currentX_ = p.x;
      this.currentY_ = p.y;
    };

    contextPrototype.bezierCurveTo = function (
      aCP1x,
      aCP1y,
      aCP2x,
      aCP2y,
      aX,
      aY
    ) {
      var p = getCoords(this, aX, aY);
      var cp1 = getCoords(this, aCP1x, aCP1y);
      var cp2 = getCoords(this, aCP2x, aCP2y);
      bezierCurveTo(this, cp1, cp2, p);
    };

    // Helper function that takes the already fixed cordinates.
    function bezierCurveTo(self, cp1, cp2, p) {
      self.currentPath_.push({
        type: "bezierCurveTo",
        cp1x: cp1.x,
        cp1y: cp1.y,
        cp2x: cp2.x,
        cp2y: cp2.y,
        x: p.x,
        y: p.y,
      });
      self.currentX_ = p.x;
      self.currentY_ = p.y;
    }

    contextPrototype.quadraticCurveTo = function (aCPx, aCPy, aX, aY) {
      // the following is lifted almost directly from
      // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes

      var cp = getCoords(this, aCPx, aCPy);
      var p = getCoords(this, aX, aY);

      var cp1 = {
        x: this.currentX_ + (2.0 / 3.0) * (cp.x - this.currentX_),
        y: this.currentY_ + (2.0 / 3.0) * (cp.y - this.currentY_),
      };
      var cp2 = {
        x: cp1.x + (p.x - this.currentX_) / 3.0,
        y: cp1.y + (p.y - this.currentY_) / 3.0,
      };

      bezierCurveTo(this, cp1, cp2, p);
    };

    contextPrototype.arc = function (
      aX,
      aY,
      aRadius,
      aStartAngle,
      aEndAngle,
      aClockwise
    ) {
      aRadius *= Z;
      var arcType = aClockwise ? "at" : "wa";

      var xStart = aX + mc(aStartAngle) * aRadius - Z2;
      var yStart = aY + ms(aStartAngle) * aRadius - Z2;

      var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
      var yEnd = aY + ms(aEndAngle) * aRadius - Z2;

      // IE won't render arches drawn counter clockwise if xStart == xEnd.
      if (xStart == xEnd && !aClockwise) {
        xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
        // that can be represented in binary
      }

      var p = getCoords(this, aX, aY);
      var pStart = getCoords(this, xStart, yStart);
      var pEnd = getCoords(this, xEnd, yEnd);

      this.currentPath_.push({
        type: arcType,
        x: p.x,
        y: p.y,
        radius: aRadius,
        xStart: pStart.x,
        yStart: pStart.y,
        xEnd: pEnd.x,
        yEnd: pEnd.y,
      });
    };

    contextPrototype.rect = function (aX, aY, aWidth, aHeight) {
      this.moveTo(aX, aY);
      this.lineTo(aX + aWidth, aY);
      this.lineTo(aX + aWidth, aY + aHeight);
      this.lineTo(aX, aY + aHeight);
      this.closePath();
    };

    contextPrototype.strokeRect = function (aX, aY, aWidth, aHeight) {
      var oldPath = this.currentPath_;
      this.beginPath();

      this.moveTo(aX, aY);
      this.lineTo(aX + aWidth, aY);
      this.lineTo(aX + aWidth, aY + aHeight);
      this.lineTo(aX, aY + aHeight);
      this.closePath();
      this.stroke();

      this.currentPath_ = oldPath;
    };

    contextPrototype.fillRect = function (aX, aY, aWidth, aHeight) {
      var oldPath = this.currentPath_;
      this.beginPath();

      this.moveTo(aX, aY);
      this.lineTo(aX + aWidth, aY);
      this.lineTo(aX + aWidth, aY + aHeight);
      this.lineTo(aX, aY + aHeight);
      this.closePath();
      this.fill();

      this.currentPath_ = oldPath;
    };

    contextPrototype.createLinearGradient = function (aX0, aY0, aX1, aY1) {
      var gradient = new CanvasGradient_("gradient");
      gradient.x0_ = aX0;
      gradient.y0_ = aY0;
      gradient.x1_ = aX1;
      gradient.y1_ = aY1;
      return gradient;
    };

    contextPrototype.createRadialGradient = function (
      aX0,
      aY0,
      aR0,
      aX1,
      aY1,
      aR1
    ) {
      var gradient = new CanvasGradient_("gradientradial");
      gradient.x0_ = aX0;
      gradient.y0_ = aY0;
      gradient.r0_ = aR0;
      gradient.x1_ = aX1;
      gradient.y1_ = aY1;
      gradient.r1_ = aR1;
      return gradient;
    };

    contextPrototype.drawImage = function (image, var_args) {
      var dx, dy, dw, dh, sx, sy, sw, sh;

      // to find the original width we overide the width and height
      var oldRuntimeWidth = image.runtimeStyle.width;
      var oldRuntimeHeight = image.runtimeStyle.height;
      image.runtimeStyle.width = "auto";
      image.runtimeStyle.height = "auto";

      // get the original size
      var w = image.width;
      var h = image.height;

      // and remove overides
      image.runtimeStyle.width = oldRuntimeWidth;
      image.runtimeStyle.height = oldRuntimeHeight;

      if (arguments.length == 3) {
        dx = arguments[1];
        dy = arguments[2];
        sx = sy = 0;
        sw = dw = w;
        sh = dh = h;
      } else if (arguments.length == 5) {
        dx = arguments[1];
        dy = arguments[2];
        dw = arguments[3];
        dh = arguments[4];
        sx = sy = 0;
        sw = w;
        sh = h;
      } else if (arguments.length == 9) {
        sx = arguments[1];
        sy = arguments[2];
        sw = arguments[3];
        sh = arguments[4];
        dx = arguments[5];
        dy = arguments[6];
        dw = arguments[7];
        dh = arguments[8];
      } else {
        throw Error("Invalid number of arguments");
      }

      var d = getCoords(this, dx, dy);

      var w2 = sw / 2;
      var h2 = sh / 2;

      var vmlStr = [];

      var W = 10;
      var H = 10;

      // For some reason that I've now forgotten, using divs didn't work
      vmlStr.push(
        " <g_vml_:group",
        ' coordsize="',
        Z * W,
        ",",
        Z * H,
        '"',
        ' coordorigin="0,0"',
        ' style="width:',
        W,
        "px;height:",
        H,
        "px;position:absolute;"
      );

      // If filters are necessary (rotation exists), create them
      // filters are bog-slow, so only create them if abbsolutely necessary
      // The following check doesn't account for skews (which don't exist
      // in the canvas spec (yet) anyway.

      if (
        this.m_[0][0] != 1 ||
        this.m_[0][1] ||
        this.m_[1][1] != 1 ||
        this.m_[1][0]
      ) {
        var filter = [];

        // Note the 12/21 reversal
        filter.push(
          "M11=",
          this.m_[0][0],
          ",",
          "M12=",
          this.m_[1][0],
          ",",
          "M21=",
          this.m_[0][1],
          ",",
          "M22=",
          this.m_[1][1],
          ",",
          "Dx=",
          mr(d.x / Z),
          ",",
          "Dy=",
          mr(d.y / Z),
          ""
        );

        // Bounding box calculation (need to minimize displayed area so that
        // filters don't waste time on unused pixels.
        var max = d;
        var c2 = getCoords(this, dx + dw, dy);
        var c3 = getCoords(this, dx, dy + dh);
        var c4 = getCoords(this, dx + dw, dy + dh);

        max.x = m.max(max.x, c2.x, c3.x, c4.x);
        max.y = m.max(max.y, c2.y, c3.y, c4.y);

        vmlStr.push(
          "padding:0 ",
          mr(max.x / Z),
          "px ",
          mr(max.y / Z),
          "px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",
          filter.join(""),
          ", sizingmethod='clip');"
        );
      } else {
        vmlStr.push("top:", mr(d.y / Z), "px;left:", mr(d.x / Z), "px;");
      }

      vmlStr.push(
        ' ">',
        '<g_vml_:image src="',
        image.src,
        '"',
        ' style="width:',
        Z * dw,
        "px;",
        " height:",
        Z * dh,
        'px"',
        ' cropleft="',
        sx / w,
        '"',
        ' croptop="',
        sy / h,
        '"',
        ' cropright="',
        (w - sx - sw) / w,
        '"',
        ' cropbottom="',
        (h - sy - sh) / h,
        '"',
        " />",
        "</g_vml_:group>"
      );

      this.element_.insertAdjacentHTML("BeforeEnd", vmlStr.join(""));
    };

    contextPrototype.stroke = function (aFill) {
      var W = 10;
      var H = 10;
      // Divide the shape into chunks if it's too long because IE has a limit
      // somewhere for how long a VML shape can be. This simple division does
      // not work with fills, only strokes, unfortunately.
      var chunkSize = 5000;

      var min = { x: null, y: null };
      var max = { x: null, y: null };

      for (var j = 0; j < this.currentPath_.length; j += chunkSize) {
        var lineStr = [];
        var lineOpen = false;

        lineStr.push(
          "<g_vml_:shape",
          ' filled="',
          !!aFill,
          '"',
          ' style="position:absolute;width:',
          W,
          "px;height:",
          H,
          'px;"',
          ' coordorigin="0,0"',
          ' coordsize="',
          Z * W,
          ",",
          Z * H,
          '"',
          ' stroked="',
          !aFill,
          '"',
          ' path="'
        );

        var newSeq = false;

        for (
          var i = j;
          i < Math.min(j + chunkSize, this.currentPath_.length);
          i++
        ) {
          if (i % chunkSize == 0 && i > 0) {
            // move into position for next chunk
            lineStr.push(
              " m ",
              mr(this.currentPath_[i - 1].x),
              ",",
              mr(this.currentPath_[i - 1].y)
            );
          }

          var p = this.currentPath_[i];
          var c;

          switch (p.type) {
            case "moveTo":
              c = p;
              lineStr.push(" m ", mr(p.x), ",", mr(p.y));
              break;
            case "lineTo":
              lineStr.push(" l ", mr(p.x), ",", mr(p.y));
              break;
            case "close":
              lineStr.push(" x ");
              p = null;
              break;
            case "bezierCurveTo":
              lineStr.push(
                " c ",
                mr(p.cp1x),
                ",",
                mr(p.cp1y),
                ",",
                mr(p.cp2x),
                ",",
                mr(p.cp2y),
                ",",
                mr(p.x),
                ",",
                mr(p.y)
              );
              break;
            case "at":
            case "wa":
              lineStr.push(
                " ",
                p.type,
                " ",
                mr(p.x - this.arcScaleX_ * p.radius),
                ",",
                mr(p.y - this.arcScaleY_ * p.radius),
                " ",
                mr(p.x + this.arcScaleX_ * p.radius),
                ",",
                mr(p.y + this.arcScaleY_ * p.radius),
                " ",
                mr(p.xStart),
                ",",
                mr(p.yStart),
                " ",
                mr(p.xEnd),
                ",",
                mr(p.yEnd)
              );
              break;
          }

          // TODO: Following is broken for curves due to
          //       move to proper paths.

          // Figure out dimensions so we can do gradient fills
          // properly
          if (p) {
            if (min.x == null || p.x < min.x) {
              min.x = p.x;
            }
            if (max.x == null || p.x > max.x) {
              max.x = p.x;
            }
            if (min.y == null || p.y < min.y) {
              min.y = p.y;
            }
            if (max.y == null || p.y > max.y) {
              max.y = p.y;
            }
          }
        }
        lineStr.push(' ">');

        if (!aFill) {
          appendStroke(this, lineStr);
        } else {
          appendFill(this, lineStr, min, max);
        }

        lineStr.push("</g_vml_:shape>");

        this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));
      }
    };

    function appendStroke(ctx, lineStr) {
      var a = processStyle(ctx.strokeStyle);
      var color = a.color;
      var opacity = a.alpha * ctx.globalAlpha;
      var lineWidth = ctx.lineScale_ * ctx.lineWidth;

      // VML cannot correctly render a line if the width is less than 1px.
      // In that case, we dilute the color to make the line look thinner.
      if (lineWidth < 1) {
        opacity *= lineWidth;
      }

      lineStr.push(
        "<g_vml_:stroke",
        ' opacity="',
        opacity,
        '"',
        ' joinstyle="',
        ctx.lineJoin,
        '"',
        ' miterlimit="',
        ctx.miterLimit,
        '"',
        ' endcap="',
        processLineCap(ctx.lineCap),
        '"',
        ' weight="',
        lineWidth,
        'px"',
        ' color="',
        color,
        '" />'
      );
    }

    function appendFill(ctx, lineStr, min, max) {
      var fillStyle = ctx.fillStyle;
      var arcScaleX = ctx.arcScaleX_;
      var arcScaleY = ctx.arcScaleY_;
      var width = max.x - min.x;
      var height = max.y - min.y;
      if (fillStyle instanceof CanvasGradient_) {
        // TODO: Gradients transformed with the transformation matrix.
        var angle = 0;
        var focus = { x: 0, y: 0 };

        // additional offset
        var shift = 0;
        // scale factor for offset
        var expansion = 1;

        if (fillStyle.type_ == "gradient") {
          var x0 = fillStyle.x0_ / arcScaleX;
          var y0 = fillStyle.y0_ / arcScaleY;
          var x1 = fillStyle.x1_ / arcScaleX;
          var y1 = fillStyle.y1_ / arcScaleY;
          var p0 = getCoords(ctx, x0, y0);
          var p1 = getCoords(ctx, x1, y1);
          var dx = p1.x - p0.x;
          var dy = p1.y - p0.y;
          angle = (Math.atan2(dx, dy) * 180) / Math.PI;

          // The angle should be a non-negative number.
          if (angle < 0) {
            angle += 360;
          }

          // Very small angles produce an unexpected result because they are
          // converted to a scientific notation string.
          if (angle < 1e-6) {
            angle = 0;
          }
        } else {
          var p0 = getCoords(ctx, fillStyle.x0_, fillStyle.y0_);
          focus = {
            x: (p0.x - min.x) / width,
            y: (p0.y - min.y) / height,
          };

          width /= arcScaleX * Z;
          height /= arcScaleY * Z;
          var dimension = m.max(width, height);
          shift = (2 * fillStyle.r0_) / dimension;
          expansion = (2 * fillStyle.r1_) / dimension - shift;
        }

        // We need to sort the color stops in ascending order by offset,
        // otherwise IE won't interpret it correctly.
        var stops = fillStyle.colors_;
        stops.sort(function (cs1, cs2) {
          return cs1.offset - cs2.offset;
        });

        var length = stops.length;
        var color1 = stops[0].color;
        var color2 = stops[length - 1].color;
        var opacity1 = stops[0].alpha * ctx.globalAlpha;
        var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;

        var colors = [];
        for (var i = 0; i < length; i++) {
          var stop = stops[i];
          colors.push(stop.offset * expansion + shift + " " + stop.color);
        }

        // When colors attribute is used, the meanings of opacity and o:opacity2
        // are reversed.
        lineStr.push(
          '<g_vml_:fill type="',
          fillStyle.type_,
          '"',
          ' method="none" focus="100%"',
          ' color="',
          color1,
          '"',
          ' color2="',
          color2,
          '"',
          ' colors="',
          colors.join(","),
          '"',
          ' opacity="',
          opacity2,
          '"',
          ' g_o_:opacity2="',
          opacity1,
          '"',
          ' angle="',
          angle,
          '"',
          ' focusposition="',
          focus.x,
          ",",
          focus.y,
          '" />'
        );
      } else if (fillStyle instanceof CanvasPattern_) {
        if (width && height) {
          var deltaLeft = -min.x;
          var deltaTop = -min.y;
          lineStr.push(
            "<g_vml_:fill",
            ' position="',
            (deltaLeft / width) * arcScaleX * arcScaleX,
            ",",
            (deltaTop / height) * arcScaleY * arcScaleY,
            '"',
            ' type="tile"',
            // TODO: Figure out the correct size to fit the scale.
            //' size="', w, 'px ', h, 'px"',
            ' src="',
            fillStyle.src_,
            '" />'
          );
        }
      } else {
        var a = processStyle(ctx.fillStyle);
        var color = a.color;
        var opacity = a.alpha * ctx.globalAlpha;
        lineStr.push(
          '<g_vml_:fill color="',
          color,
          '" opacity="',
          opacity,
          '" />'
        );
      }
    }

    contextPrototype.fill = function () {
      this.stroke(true);
    };

    contextPrototype.closePath = function () {
      this.currentPath_.push({ type: "close" });
    };

    function getCoords(ctx, aX, aY) {
      var m = ctx.m_;
      return {
        x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
        y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2,
      };
    }

    contextPrototype.save = function () {
      var o = {};
      copyState(this, o);
      this.aStack_.push(o);
      this.mStack_.push(this.m_);
      this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
    };

    contextPrototype.restore = function () {
      if (this.aStack_.length) {
        copyState(this.aStack_.pop(), this);
        this.m_ = this.mStack_.pop();
      }
    };

    function matrixIsFinite(m) {
      return (
        isFinite(m[0][0]) &&
        isFinite(m[0][1]) &&
        isFinite(m[1][0]) &&
        isFinite(m[1][1]) &&
        isFinite(m[2][0]) &&
        isFinite(m[2][1])
      );
    }

    function setM(ctx, m, updateLineScale) {
      if (!matrixIsFinite(m)) {
        return;
      }
      ctx.m_ = m;

      if (updateLineScale) {
        // Get the line scale.
        // Determinant of this.m_ means how much the area is enlarged by the
        // transformation. So its square root can be used as a scale factor
        // for width.
        var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
        ctx.lineScale_ = sqrt(abs(det));
      }
    }

    contextPrototype.translate = function (aX, aY) {
      var m1 = [
        [1, 0, 0],
        [0, 1, 0],
        [aX, aY, 1],
      ];

      setM(this, matrixMultiply(m1, this.m_), false);
    };

    contextPrototype.rotate = function (aRot) {
      var c = mc(aRot);
      var s = ms(aRot);

      var m1 = [
        [c, s, 0],
        [-s, c, 0],
        [0, 0, 1],
      ];

      setM(this, matrixMultiply(m1, this.m_), false);
    };

    contextPrototype.scale = function (aX, aY) {
      this.arcScaleX_ *= aX;
      this.arcScaleY_ *= aY;
      var m1 = [
        [aX, 0, 0],
        [0, aY, 0],
        [0, 0, 1],
      ];

      setM(this, matrixMultiply(m1, this.m_), true);
    };

    contextPrototype.transform = function (m11, m12, m21, m22, dx, dy) {
      var m1 = [
        [m11, m12, 0],
        [m21, m22, 0],
        [dx, dy, 1],
      ];

      setM(this, matrixMultiply(m1, this.m_), true);
    };

    contextPrototype.setTransform = function (m11, m12, m21, m22, dx, dy) {
      var m = [
        [m11, m12, 0],
        [m21, m22, 0],
        [dx, dy, 1],
      ];

      setM(this, m, true);
    };

    /**
     * The text drawing function.
     * The maxWidth argument isn't taken in account, since no browser supports
     * it yet.
     */
    contextPrototype.drawText_ = function (text, x, y, maxWidth, stroke) {
      var m = this.m_,
        delta = 1000,
        left = 0,
        right = delta,
        offset = { x: 0, y: 0 },
        lineStr = [];

      var fontStyle = getComputedStyle(
        processFontStyle(this.font),
        this.element_
      );

      var fontStyleString = buildStyle(fontStyle);

      var elementStyle = this.element_.currentStyle;
      var textAlign = this.textAlign.toLowerCase();
      switch (textAlign) {
        case "left":
        case "center":
        case "right":
          break;
        case "end":
          textAlign = elementStyle.direction == "ltr" ? "right" : "left";
          break;
        case "start":
          textAlign = elementStyle.direction == "rtl" ? "right" : "left";
          break;
        default:
          textAlign = "left";
      }

      // 1.75 is an arbitrary number, as there is no info about the text baseline
      switch (this.textBaseline) {
        case "hanging":
        case "top":
          offset.y = fontStyle.size / 1.75;
          break;
        case "middle":
          break;
        default:
        case null:
        case "alphabetic":
        case "ideographic":
        case "bottom":
          offset.y = -fontStyle.size / 2.25;
          break;
      }

      switch (textAlign) {
        case "right":
          left = delta;
          right = 0.05;
          break;
        case "center":
          left = right = delta / 2;
          break;
      }

      var d = getCoords(this, x + offset.x, y + offset.y);

      lineStr.push(
        '<g_vml_:line from="',
        -left,
        ' 0" to="',
        right,
        ' 0.05" ',
        ' coordsize="100 100" coordorigin="0 0"',
        ' filled="',
        !stroke,
        '" stroked="',
        !!stroke,
        '" style="position:absolute;width:1px;height:1px;">'
      );

      if (stroke) {
        appendStroke(this, lineStr);
      } else {
        // TODO: Fix the min and max params.
        appendFill(
          this,
          lineStr,
          { x: -left, y: 0 },
          { x: right, y: fontStyle.size }
        );
      }

      var skewM =
        m[0][0].toFixed(3) +
        "," +
        m[1][0].toFixed(3) +
        "," +
        m[0][1].toFixed(3) +
        "," +
        m[1][1].toFixed(3) +
        ",0,0";

      var skewOffset = mr(d.x / Z) + "," + mr(d.y / Z);

      lineStr.push(
        '<g_vml_:skew on="t" matrix="',
        skewM,
        '" ',
        ' offset="',
        skewOffset,
        '" origin="',
        left,
        ' 0" />',
        '<g_vml_:path textpathok="true" />',
        '<g_vml_:textpath on="true" string="',
        encodeHtmlAttribute(text),
        '" style="v-text-align:',
        textAlign,
        ";font:",
        encodeHtmlAttribute(fontStyleString),
        '" /></g_vml_:line>'
      );

      this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));
    };

    contextPrototype.fillText = function (text, x, y, maxWidth) {
      this.drawText_(text, x, y, maxWidth, false);
    };

    contextPrototype.strokeText = function (text, x, y, maxWidth) {
      this.drawText_(text, x, y, maxWidth, true);
    };

    contextPrototype.measureText = function (text) {
      if (!this.textMeasureEl_) {
        var s =
          '<span style="position:absolute;' +
          "top:-20000px;left:0;padding:0;margin:0;border:none;" +
          'white-space:pre;"></span>';
        this.element_.insertAdjacentHTML("beforeEnd", s);
        this.textMeasureEl_ = this.element_.lastChild;
      }
      var doc = this.element_.ownerDocument;
      this.textMeasureEl_.innerHTML = "";
      this.textMeasureEl_.style.font = this.font;
      // Don't use innerHTML or innerText because they allow markup/whitespace.
      this.textMeasureEl_.appendChild(doc.createTextNode(text));
      return { width: this.textMeasureEl_.offsetWidth };
    };

    /******** STUBS ********/
    contextPrototype.clip = function () {
      // TODO: Implement
    };

    contextPrototype.arcTo = function () {
      // TODO: Implement
    };

    contextPrototype.createPattern = function (image, repetition) {
      return new CanvasPattern_(image, repetition);
    };

    // Gradient / Pattern Stubs
    function CanvasGradient_(aType) {
      this.type_ = aType;
      this.x0_ = 0;
      this.y0_ = 0;
      this.r0_ = 0;
      this.x1_ = 0;
      this.y1_ = 0;
      this.r1_ = 0;
      this.colors_ = [];
    }

    CanvasGradient_.prototype.addColorStop = function (aOffset, aColor) {
      aColor = processStyle(aColor);
      this.colors_.push({
        offset: aOffset,
        color: aColor.color,
        alpha: aColor.alpha,
      });
    };

    function CanvasPattern_(image, repetition) {
      assertImageIsValid(image);
      switch (repetition) {
        case "repeat":
        case null:
        case "":
          this.repetition_ = "repeat";
          break;
        case "repeat-x":
        case "repeat-y":
        case "no-repeat":
          this.repetition_ = repetition;
          break;
        default:
          throwException("SYNTAX_ERR");
      }

      this.src_ = image.src;
      this.width_ = image.width;
      this.height_ = image.height;
    }

    function throwException(s) {
      throw new DOMException_(s);
    }

    function assertImageIsValid(img) {
      if (!img || img.nodeType != 1 || img.tagName != "IMG") {
        throwException("TYPE_MISMATCH_ERR");
      }
      if (img.readyState != "complete") {
        throwException("INVALID_STATE_ERR");
      }
    }

    function DOMException_(s) {
      this.code = this[s];
      this.message = s + ": DOM Exception " + this.code;
    }
    var p = (DOMException_.prototype = new Error());
    p.INDEX_SIZE_ERR = 1;
    p.DOMSTRING_SIZE_ERR = 2;
    p.HIERARCHY_REQUEST_ERR = 3;
    p.WRONG_DOCUMENT_ERR = 4;
    p.INVALID_CHARACTER_ERR = 5;
    p.NO_DATA_ALLOWED_ERR = 6;
    p.NO_MODIFICATION_ALLOWED_ERR = 7;
    p.NOT_FOUND_ERR = 8;
    p.NOT_SUPPORTED_ERR = 9;
    p.INUSE_ATTRIBUTE_ERR = 10;
    p.INVALID_STATE_ERR = 11;
    p.SYNTAX_ERR = 12;
    p.INVALID_MODIFICATION_ERR = 13;
    p.NAMESPACE_ERR = 14;
    p.INVALID_ACCESS_ERR = 15;
    p.VALIDATION_ERR = 16;
    p.TYPE_MISMATCH_ERR = 17;

    // set up externs
    G_vmlCanvasManager = G_vmlCanvasManager_;
    CanvasRenderingContext2D = CanvasRenderingContext2D_;
    CanvasGradient = CanvasGradient_;
    CanvasPattern = CanvasPattern_;
    DOMException = DOMException_;
  } // if
})();
