// 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
if (!document.createElement('canvas').getContext) {

  (function () {

    // 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
    };

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