"use strict";

/* DrawingContext.js
 *
 * Author: Dmitry.Sokolov@avsmedia.net
 * Date:   Nov 21, 2011
 */
(function (/** jQuery */$, /** Window */window, undefined) {

/*
 * Import
 * -----------------------------------------------------------------------------
 */

var asc = window["Asc"];
var asc_round = asc.round;
var asc_floor = asc.floor;

function colorObjToAscColor(color) {
	var oRes = null;
	var r = color.getR();
	var g = color.getG();
	var b = color.getB();
	var bTheme = false;
	if(color instanceof ThemeColor && null != color.theme)
	{
		var array_colors_types = [6, 15, 7, 16, 0, 1, 2, 3, 4, 5];
		var themePresentation = array_colors_types[color.theme];
		var tintExcel = 0;
		if(null != color.tint)
			tintExcel = color.tint;
		var tintPresentation = 0;
		var oThemeColorTint = g_oThemeColorTint[color.theme];
		if(null != oThemeColorTint)
		{
			for(var i = 0 , length = oThemeColorTint.length; i < length; ++i)
			{
				var cur = oThemeColorTint[i];
				//0.005 установлено экспериментально
				if(Math.abs(cur - tintExcel) < 0.005)
				{
					bTheme = true;
					tintPresentation = i;
					break;
				}
			}
		}
		if(bTheme)
		{
			oRes = new CAscColor();
			oRes.r = r;
			oRes.g = g;
			oRes.b = b;
			oRes.a = 255;
			oRes.type = c_oAscColor.COLOR_TYPE_SCHEME;
			oRes.value = themePresentation;
		}
	}
	if(false == bTheme)
		oRes = CreateAscColorCustom(r, g, b);
	return oRes;
}

var oldPpi = undefined,
    cvt = undefined;

/**
 * Gets ratio to convert units
 * @param {Number} fromUnits  Units (0=px, 1=pt, 2=in, 3=mm)
 * @param {Number} toUnits    Units (0=px, 1=pt, 2=in, 3=mm)
 * @param {Number} ppi        Points per inch
 * @return {Number}  Ratio
 */
function getCvtRatio(fromUnits, toUnits, ppi) {
	if (ppi !== oldPpi || oldPpi === undefined) {
		var _ppi  = 1 / ppi,
		    _72   = 1 / 72,
		    _25_4 = 1 / 25.4;
		cvt = [
			     /*    px          pt       in        mm    */
			/*px*/[         1,   72*_ppi,   _ppi,  25.4*_ppi ],
			/*pt*/[   ppi*_72,         1,    _72,   25.4*_72 ],
			/*in*/[       ppi,        72,      1,       25.4 ],
			/*mm*/[ ppi*_25_4,  72*_25_4,  _25_4,          1 ]
		];
		oldPpi = ppi;
	}
	return cvt[fromUnits][toUnits];
}

/**
 * Округляет текущее значение в pt таким образом, чтобы при переводе его в px при указанном DPI получалось
 * целое число пикселей
 * @param {type} origPt
 * @param {type} ppi
 * @param {type} pxAddon
 * @returns {Number}
 */
function calcNearestPt(origPt, ppi, pxAddon) {
	var a = pxAddon !== undefined ? pxAddon : 0,
	    x = origPt * ppi / 72,
	    y = x | x,
	    p = x - y < .000000001 ? 0 : 1; // to fix float number precision caused by binary presentation
	return (y + p + a) / ppi * 72;
}

function deg2rad(deg){
	return deg * Math.PI / 180.0;
}

function rad2deg(rad){
	return rad * 180.0 / Math.PI;
}



/** @const */
var MATRIX_ORDER_PREPEND = 0,
    MATRIX_ORDER_APPEND  = 1;

/**
 * @constructor
 */
function Matrix() {
	if ( !(this instanceof Matrix) ) {
		return new Matrix();
	}

	this.sx  = 1.0;
	this.shx = 0.0;
	this.shy = 0.0;
	this.sy  = 1.0;
	this.tx  = 0.0;
	this.ty  = 0.0;

	return this;
}

Matrix.prototype = {

	/** @type Matrix */
	constructor: Matrix,

	reset: function () {
		this.sx  = 1.0;
		this.shx = 0.0;
		this.shy = 0.0;
		this.sy  = 1.0;
		this.tx  = 0.0;
		this.ty  = 0.0;
	},

	assign: function (sx, shx, shy, sy, tx, ty) {
		this.sx  = sx;
		this.shx = shx;
		this.shy = shy;
		this.sy  = sy;
		this.tx  = tx;
		this.ty  = ty;
	},

	copyFrom: function (matrix) {
		this.sx  = matrix.sx;
		this.shx = matrix.shx;
		this.shy = matrix.shy;
		this.sy  = matrix.sy;
		this.tx  = matrix.tx;
		this.ty  = matrix.ty;
	},

	clone: function () {
		var m = new Matrix();
		m.copyFrom(this);
		return m;
	},

	multiply: function (matrix, order) {
		if (MATRIX_ORDER_PREPEND === order) {
			var m = matrix.clone();
			m.multiply(this, MATRIX_ORDER_APPEND);
			this.copyFrom(m);
		} else {
			var t0   = this.sx  * matrix.sx  + this.shy * matrix.shx;
			var t2   = this.shx * matrix.sx  + this.sy  * matrix.shx;
			var t4   = this.tx  * matrix.sx  + this.ty  * matrix.shx + matrix.tx;
			this.shy = this.sx  * matrix.shy + this.shy * matrix.sy;
			this.sy  = this.shx * matrix.shy + this.sy  * matrix.sy;
			this.ty  = this.tx  * matrix.shy + this.ty  * matrix.sy + matrix.ty;
			this.sx  = t0;
			this.shx = t2;
			this.tx  = t4;
		}
	},

	translate: function (x, y, order) {
		var m = new Matrix();
		m.tx  = x;
		m.ty  = y;
		this.multiply(m, order);
	},

	scale: function (x, y, order) {
		var m = new Matrix();
		m.sx  = x;
		m.sy  = y;
		this.multiply(m, order);
	},

	rotate: function (a, order) {
		var m = new Matrix();
		var rad = deg2rad(a);
		m.sx  = Math.cos(rad);
		m.shx = Math.sin(rad);
		m.shy = -Math.sin(rad);
		m.sy  = Math.cos(rad);
		this.multiply(m, order);
	},

	rotateAt: function (a, x, y, order) {
		this.translate(-x, -y, order);
		this.rotate(a, order);
		this.translate(x, y, order);
	},

	determinant: function () {
		return this.sx * this.sy - this.shy * this.shx;
	},

	invert: function () {
		var det = this.determinant();
		if (0.0001 > det) {return;}
		var d = 1 / det;

		var t0 = this.sy * d;
		this.sy =  this.sx * d;
		this.shy = -this.shy * d;
		this.shx = -this.shx * d;

		var t4 = -this.tx * t0  - this.ty * this.shx;
		this.ty = -this.tx * this.shy - this.ty * this.sy;

		this.sx = t0;
		this.tx = t4;
	},

	transformPointX: function (x, y) {
		return x * this.sx  + y * this.shx + this.tx;
	},

	transformPointY: function (x, y) {
		return x * this.shy + y * this.sy  + this.ty;
	},

	/** Calculates rotation angle */
	getRotation: function () {
		var x1 = 0.0;
		var y1 = 0.0;
		var x2 = 1.0;
		var y2 = 0.0;
		this.transformPoint(x1, y1);
		this.transformPoint(x2, y2);
		var a = Math.atan2(y2-y1, x2-x1);
		return rad2deg(a);
	}

};



/**
 * Creates font properties
 * -----------------------------------------------------------------------------
 * @constructor
 * @param {String} family     Font family
 * @param {Number} size       Font size
 * @param {Boolean} bold      Font style - bold
 * @param {Boolean} italic    Font style - italic
 * @param {String} underline  Font style - type of underline
 * @param {String} strikeout  Font style - type of strike-out
 *
 * @memberOf Asc
 */
function FontProperties(family, size, bold, italic, underline, strikeout) {
	if ( !(this instanceof FontProperties) ) {
		return new FontProperties(family, size, bold, italic, underline, strikeout);
	}

	this.FontFamily = {Name: family, Index: -1, Angle : 0};
	this.FontSize   = size;
	this.Bold       = !!bold;
	this.Italic     = !!italic;
	this.Underline  = underline;
	this.Strikeout  = strikeout;

	return this;
}

FontProperties.prototype = {

	/** @type FontProperties */
	constructor: FontProperties,

	/**
	 * Assigns font preperties from another object
	 * @param {FontProperties} font
	 */
	copyFrom: function (font) {
		this.FontFamily.Name  = font.FontFamily.Name;
		this.FontFamily.Index = font.FontFamily.Index;
		this.FontSize  = font.FontSize;
		this.Bold      = font.Bold;
		this.Italic    = font.Italic;
		this.Underline = font.Underline;
		this.Strikeout = font.Strikeout;
	},

	/** @return {FontProperties} */
	clone: function () {
		return new FontProperties(this.FontFamily.Name, this.FontSize,
				this.Bold, this.Italic, this.Underline, this.Strikeout);
	},

	isEqual: function (font) {
		return font !== undefined &&
		       this.FontFamily.Name.toLowerCase() === font.FontFamily.Name.toLowerCase() &&
		       this.FontSize === font.FontSize &&
		       this.Bold === font.Bold &&
		       this.Italic === font.Italic;
	}

};



/**
 * Creates text metrics
 * -----------------------------------------------------------------------------
 * @constructor
 * @param {Number} width
 * @param {Number} height
 * @param {Number} lineHeight
 * @param {Number} baseline
 * @param {Number} descender
 * @param {Number} fontSize
 * @param {Number} centerline
 * @param {Number} widthBB
 *
 * @memberOf Asc
 */
function TextMetrics(width, height, lineHeight, baseline, descender, fontSize, centerline, widthBB) {
	if ( !(this instanceof TextMetrics) ) {
		return new TextMetrics(width, height, lineHeight, baseline, descender, fontSize, centerline, widthBB);
	}

	this.width      = width !== undefined ? width : 0;
	this.height     = height !== undefined ? height : 0;
	this.lineHeight = lineHeight !== undefined ? lineHeight : 0;
	this.baseline   = baseline !== undefined ? baseline : 0;
	this.descender  = descender !== undefined ? descender : 0;
	this.fontSize   = fontSize !== undefined ? fontSize : 0;
	this.centerline = centerline !== undefined ? centerline : 0;
	this.widthBB    = widthBB !== undefined ? widthBB : 0;

	return this;
}



/**
 * Creates font metrics
 * -----------------------------------------------------------------------------
 * @constructor
 * @param {Number} ascender
 * @param {Number} descender
 * @param {Number} lineGap
 *
 * @memberOf Asc
 */
function FontMetrics(ascender, descender, lineGap) {
	if ( !(this instanceof FontMetrics) ) {
		return new FontMetrics(ascender, descender, lineGap);
	}

	this.ascender  = ascender !== undefined ? ascender : 0;
	this.descender = descender !== undefined ? descender : 0;
	this.lineGap   = lineGap !== undefined ? lineGap : 0;

	return this;
}

FontMetrics.prototype.clone = function () {
	return new FontMetrics(this.ascender, this.descender, this.lineGap);
};


/**
 * Emulates scalable canvas context
 * -----------------------------------------------------------------------------
 * @constructor
 * @param {Object} settings  Settings : {
 *   canvas : HTMLElement
 *   units  : units (0=px, 1=pt, 2=in, 3=mm)
 *   font   : FontProperties
 * }
 *
 * @memberOf Asc
 */
function DrawingContext(settings) {
	if ( !(this instanceof DrawingContext) ) {
		return new DrawingContext(settings);
	}
	this.canvas = null;
	this.ctx = null;

	this.setCanvas(settings.canvas);

	var ppiTest =
			$('<div style="position: absolute; width: 10in; height:10in; visibility:hidden; padding:0;"/>')
			.appendTo("body");
	this.ppiX = asc_round(ppiTest[0] ? (ppiTest[0].offsetWidth * 0.1) : 96);
	this.ppiY = asc_round(ppiTest[0] ? (ppiTest[0].offsetHeight * 0.1) : 96);
	ppiTest.remove();

	this._mct  = new Matrix();  // units transform
	this._mt   = new Matrix();  // user transform
    this._mbt  = new Matrix();  // bound transform
	this._mft  = new Matrix();  // full transform
	this._mift = new Matrix();  // inverted full transform
    this._im   = new Matrix();

	this.scaleFactor = 1;

	this._1px_x = getCvtRatio(0/*px*/, 3/*mm*/, this.ppiX);
	this._1px_y = getCvtRatio(0/*px*/, 3/*mm*/, this.ppiY);
	this.units  = 3/*mm*/;
	this.changeUnits(undefined !== settings.units ? settings.units : this.units);

	this.fmgrGraphics = undefined !== settings.fmgrGraphics ? settings.fmgrGraphics : null;
	if (null === this.fmgrGraphics) {return null;}

	/** @type FontProperties */
	this.font = undefined !== settings.font ? settings.font : null;
	// Font должен быть передан (он общий для всех DrawingContext, т.к. может возникнуть ситуация как в баге http://bugzserver/show_bug.cgi?id=19784)
	if (null === this.font) {return null;}

	// CColor
	this.fillColor = new CColor(255, 255, 255);
    return this;
}

DrawingContext.prototype = {

	/** @type DrawingContext */
	constructor: DrawingContext,

	/**
	 * Returns width of drawing context in current units
	 * @param {Number} units  Единицы измерения (0=px, 1=pt, 2=in, 3=mm) в которых будет возвращена ширина
	 * @return {Number}
	 */
	getWidth: function (units) {
		var i = units >= 0 && units <=3 ? units : this.units;
		return this.canvas.width * getCvtRatio(0/*px*/, i, this.ppiX);
	},

	/**
	 * Returns height of drawing context in current units
	 * @param {Number} units  Единицы измерения (0=px, 1=pt, 2=in, 3=mm) в которых будет возвращена высота
	 * @return {Number}
	 */
	getHeight: function (units) {
		var i = units >= 0 && units <=3 ? units : this.units;
		return this.canvas.height * getCvtRatio(0/*px*/, i, this.ppiY);
	},

	/**
	 * Returns canvas element
	 * @type {Element}
	 */
	getCanvas: function () {
		return this.canvas;
	},

	/**
	 *
	 * @param canvas
	 */
	setCanvas: function (canvas) {
		if (null == canvas) {return;}
		this.canvas = canvas;
		this.ctx = this.canvas.getContext("2d");
		this.initContextSmoothing();
	},

	/**
	 * Returns pixels per inch ratio
	 * @type {Number}
	 */
	getPPIX: function () {
		return this.ppiX;
	},

	/**
	 * Returns pixels per inch ratio
	 * @type {Number}
	 */
	getPPIY: function () {
		return this.ppiY;
	},

	/**
	 * Returns currrent units (0=px, 1=pt, 2=in, 3=mm)
	 * @type {Number}
	 */
	getUnits: function () {
		return this.units;
	},

	/**
	 * Changes units of drawing context
	 * @param {Number} units  New units of drawing context (0=px, 1=pt, 2=in, 3=mm)
	 */
	changeUnits: function (units) {
		var i = units >= 0 && units <=3 ? units : 0;
		this._mct.sx = getCvtRatio(i, 0/*px*/, this.ppiX);
		this._mct.sy = getCvtRatio(i, 0/*px*/, this.ppiY);
		this._calcMFT();
		this._1px_x = getCvtRatio(0/*px*/, i, this.ppiX);
		this._1px_y = getCvtRatio(0/*px*/, i, this.ppiY);
		this.units = units;
		return this;
	},

	/**
	 * Returns currrent zoom ratio
	 * @type {Number}
	 */
	getZoom: function () {
		return this.scaleFactor;
	},

	/**
	 * Changes scale factor of drawing context by changing its PPI
	 * @param {Number} factor
	 */
	changeZoom: function (factor) {
		if (factor <= 0) {throw "Scale factor must be >= 0";}

		factor = asc_round(factor * 1000) / 1000;

		this.ppiX = asc_round(this.ppiX / this.scaleFactor * factor * 1000) / 1000;
		this.ppiY = asc_round(this.ppiY / this.scaleFactor * factor * 1000) / 1000;
		this.scaleFactor = factor;

		// reinitialize
		this.changeUnits(this.units);
		this.setFont(this.font);

		return this;
	},

	/**
	 * Resets dimensions of drawing context (canvas 'width' and 'height' attributes)
	 * @param {Number} width   New width in current units
	 * @param {Number} height  New height in current units
	 */
	resetSize: function (width, height) {
		var w = asc_round( width  * getCvtRatio(this.units, 0/*px*/, this.ppiX) ),
		    h = asc_round( height * getCvtRatio(this.units, 0/*px*/, this.ppiY) );
		if (w !== this.canvas.width) {
			this.canvas.width = w;
		}
		if (h !== this.canvas.height) {
			this.canvas.height = h;
		}
		return this;
	},

	/**
	 * Expands dimensions of drawing context (canvas 'width' and 'height' attributes)
	 * @param {Number} width   New width in current units
	 * @param {Number} height  New height in current units
	 */
	expand: function (width, height) {
		var w = asc_round( width  * getCvtRatio(this.units, 0/*px*/, this.ppiX) ),
		    h = asc_round( height * getCvtRatio(this.units, 0/*px*/, this.ppiY) );
		if (w > this.canvas.width) {
			this.canvas.width = w;
		}
		if (h > this.canvas.height) {
			this.canvas.height = h;
		}
		return this;
	},

	/**
	 * Delete smoothing
	 */
	initContextSmoothing: function () {
		var ctx = this.ctx;
		if (null === ctx)
			return;

		// Не убирать. Баг на android при scroll!!!
		if (ctx.imageSmoothingEnabled)
			ctx.imageSmoothingEnabled = false;
		if (ctx.mozImageSmoothingEnabled)
			ctx.mozImageSmoothingEnabled = false;
		if (ctx.oImageSmoothingEnabled)
			ctx.oImageSmoothingEnabled = false;
		if (ctx.webkitImageSmoothingEnabled)
			ctx.webkitImageSmoothingEnabled = false;
	},

	// Canvas methods

	clear: function () {
		this.clearRect(0, 0, this.getWidth(), this.getHeight());
		return this;
	},

	save: function () {
		this.ctx.save();
		return this;
	},

	restore: function () {
		this.ctx.restore();
		return this;
	},

	scale: function (kx, ky) {
		//TODO: implement scale()
		return this;
	},

	rotate: function (a) {
		//TODO: implement rotate()
		return this;
	},

	translate: function (dx, dy) {
		//TODO: implement translate()
		return this;
	},

	transform: function (sx, shy, shx, sy, tx, ty) {
		//TODO: implement transform()
		return this;
	},

    setTransform: function(sx, shy, shx, sy, tx, ty) {
        this._mbt.assign(sx, shx, shy, sy, tx, ty);
        return this;
    },

    setTextTransform: function(sx, shy, shx, sy, tx, ty) {
        this._mt.assign(sx, shx, shy, sy, tx, ty);
        return this;
    },

    updateTransforms: function() {
        this._calcMFT();
        this.fmgrGraphics[1].SetTextMatrix(
            this._mt.sx, this._mt.shy, this._mt.shx, this._mt.sy, this._mt.tx, this._mt.ty);
    },

    resetTransforms: function(){
        this.setTransform(this._im.sx, this._im.shy, this._im.shx, this._im.sy, this._im.tx, this._im.ty);
        this.setTextTransform(this._im.sx, this._im.shy, this._im.shx, this._im.sy, this._im.tx, this._im.ty);
        this._calcMFT();
    },

	// Style methods

	getFillStyle: function () {
		return this.ctx.fillStyle;
	},

	getStrokeStyle: function () {
		return this.ctx.strokeStyle;
	},

	getLineWidth: function () {
		return this.ctx.lineWidth;
	},

	getLineCap: function () {
		return this.ctx.lineCap;
	},

	getLineJoin: function () {
		return this.ctx.lineJoin;
	},

	/**
	 * @param {RgbColor || ThemeColor || CColor} val
	 * @returns {DrawingContext}
	 */
	setFillStyle: function (val) {
		var _r = val.getR();
		var _g = val.getG();
		var _b = val.getB();
		var _a = val.getA();
		this.fillColor = new CColor(_r, _g, _b, _a);
		this.ctx.fillStyle = "rgba(" + _r + "," + _g + "," + _b + "," + _a + ")";
		return this;
	},

	setFillPattern: function (val) {
		this.ctx.fillStyle = val;
		return this;
	},

	/**
	 * @param {RgbColor || ThemeColor || CColor} val
	 * @returns {DrawingContext}
	 */
	setStrokeStyle: function (val) {
		var _r = val.getR();
		var _g = val.getG();
		var _b = val.getB();
		var _a = val.getA();
		this.ctx.strokeStyle = "rgba(" + _r + "," + _g + "," + _b + "," + _a + ")";
		return this;
	},

	setLineWidth: function (width) {
		this.ctx.lineWidth = width;
		return this;
	},

	setLineCap: function (cap) {
		this.ctx.lineCap = cap;
		return this;
	},

	setLineJoin: function (join) {
		this.ctx.lineJoin = join;
		return this;
	},

	fillRect: function (x, y, w, h) {
		var r = this._calcRect(x, y, w, h);
		this.ctx.fillRect(r.x, r.y, r.w, r.h);
		return this;
	},

	strokeRect: function (x, y, w, h) {
		var r = this._calcRect(x, y, w, h);
		this.ctx.strokeRect(r.x+0.5, r.y+0.5, r.w-1, r.h-1);
		return this;
	},

	clearRect: function (x, y, w, h) {
		var r = this._calcRect(x, y, w, h);
		this.ctx.clearRect(r.x, r.y, r.w, r.h);
		return this;
	},

	// Font and text methods

	getFont: function () {
		return this.font.clone();
	},

	getFontFamily: function () {
		return this.font.FontFamily.Name;
	},

	getFontSize: function () {
		return this.font.FontSize;
	},

	/**
	 * @param {Number} units  Units of result (0=px, 1=pt, 2=in, 3=mm)
	 * @return {FontMetrics}
	 */
	getFontMetrics: function (units) {
		var fm = this.fmgrGraphics[0],
		    d  = Math.abs(fm.m_lDescender),
		    r  = getCvtRatio(0/*px*/, units >= 0 && units <=3 ? units : this.units, this.ppiX),
		    factor = this.getFontSize() * r / fm.m_lUnits_Per_Em;
		return new FontMetrics(
			factor * fm.m_lAscender,
			factor * d,
			factor * (fm.m_lLineHeight - fm.m_lAscender - d)
		);
	},

    setFont: function (font, angle) {
        var italic, bold, fontStyle, r;

        if (font.FontFamily.Index === undefined ||
            font.FontFamily.Index === null ||
            font.FontFamily.Index === -1) {
            font.FontFamily.Index = window.g_map_font_index[font.FontFamily.Name];
        }

        this.font.copyFrom(font);

        italic = true === font.Italic;
        bold   = true === font.Bold;

        fontStyle = FontStyle.FontStyleRegular;
        if ( !italic && bold )
            fontStyle = FontStyle.FontStyleBold;
        else if ( italic && !bold )
            fontStyle = FontStyle.FontStyleItalic;
        else if ( italic && bold )
            fontStyle = FontStyle.FontStyleBoldItalic;

        if (angle && 0 != angle) {
            r = window.g_font_infos[ font.FontFamily.Index ].LoadFont(
                window.g_font_loader, this.fmgrGraphics[1], font.FontSize, fontStyle, this.ppiX, this.ppiY);

            this.fmgrGraphics[1].SetTextMatrix(
                this._mt.sx, this._mt.shy, this._mt.shx, this._mt.sy, this._mt.tx, this._mt.ty);
        } else {
            r = window.g_font_infos[ font.FontFamily.Index ].LoadFont(
                window.g_font_loader, this.fmgrGraphics[0], font.FontSize, fontStyle, this.ppiX, this.ppiY);
        }

        if (r === false) {
            throw "Can not use " + font.FontFamily.Name + " font. (Check whether font file is loaded)";
        }

        return this;
    },

	/**
	 * Returns dimensions of first char of string
	 * @param {String} text   Character to measure
	 * @param {Number} units  Units (0 = px, 1 = pt, 2 = in, 3 = mm)
	 * @return {TextMetrics}  Returns the char dimension
	 */
	measureChar: function (text, units) {
		return this.measureText(text.charAt(0), units);
	},

	/**
	 * Returns dimensions of string
	 * @param {String} text   String to measure
	 * @param {Number} units  Units (0 = px, 1 = pt, 2 = in, 3 = mm)
	 * @return {TextMetrics}  Returns the dimension of string {width: w, height: h}
	 */
	measureText: function (text, units) {
		var fm = this.fmgrGraphics[0],
		    r  = getCvtRatio(0/*px*/, units >= 0 && units <=3 ? units : this.units, this.ppiX);
		for (var tmp, w = 0, w2 = 0, i = 0; i < text.length; ++i) {
			tmp = fm.MeasureChar( text.charCodeAt(i) );
			w += tmp.fAdvanceX;
		}
		w2 = w - tmp.fAdvanceX + tmp.oBBox.fMaxX - tmp.oBBox.fMinX + 1;
		return this._calcTextMetrics(w * r, w2 * r, fm, r);
	},
	
	getHeightText: function()
    {
        var fm = this.fmgrGraphics[0];
		var UnitsPerEm = fm.m_lUnits_Per_Em;
        var Height     = fm.m_lLineHeight;
		var setUpSize  = this.font.FontSize;
        return Height * setUpSize / UnitsPerEm;
    },

	fillGlyph: function (pGlyph, fmgr) {
		var nW = pGlyph.oBitmap.nWidth;
		var nH = pGlyph.oBitmap.nHeight;

		if ( !(nW > 0 && nH > 0) ) {return;}

		var nX = asc_floor(fmgr.m_oGlyphString.m_fX + pGlyph.fX + pGlyph.oBitmap.nX);
		var nY = asc_floor(fmgr.m_oGlyphString.m_fY + pGlyph.fY - pGlyph.oBitmap.nY);

		var _r = this.fillColor.r;
		var _g = this.fillColor.g;
		var _b = this.fillColor.b;

		if (window.g_isMobileVersion) {
			// Special for iPad (5.1)

			if (!_r && !_g && !_b) {
				this.ctx.drawImage(pGlyph.oBitmap.oGlyphData.m_oCanvas, 0, 0, nW, nH, nX, nY, nW, nH);
			} else {
				var canvD = $("<canvas width='"+nW+"' height='"+nH+"'/>")[0];
				var ctxD = canvD.getContext("2d");
				var pixDst = ctxD.getImageData(0, 0, nW, nH);
				var dstP = pixDst.data;
				var data = pGlyph.oBitmap.oGlyphData.m_oContext.getImageData(0, 0, nW, nH);
				var dataPx = data.data;
				var cur = 0;
				var cnt = 4 * nW * nH;
				for (var i = 3; i < cnt; i += 4) {
					dstP[cur++] = _r;
					dstP[cur++] = _g;
					dstP[cur++] = _b;
					dstP[cur++] = dataPx[i];
				}
				ctxD.putImageData(pixDst, 0, 0, 0, 0, nW, nH);
				this.ctx.drawImage(canvD, 0, 0, nW, nH, nX, nY, nW, nH);
			}
		} else {
			pGlyph.oBitmap.oGlyphData.checkColor(_r, _g, _b, nW, nH);
			pGlyph.oBitmap.draw(this.ctx, nX, nY);
		}
	},
	
	fillText: function (text, x, y, maxWidth, charWidths, angle) {
        var manager = angle ? this.fmgrGraphics[1] : this.fmgrGraphics[0];

        var _x = this._mift.transformPointX(x, y);
        var _y = this._mift.transformPointY(x, y);

        var length = text.length;
        for (var i = 0; i < length; ++i) {
            try {
                _x = manager.LoadString2C(text.charAt(i), _x, _y);
            } catch(err) {
                // do nothing
            }
            var pGlyph = manager.m_oGlyphString.m_pGlyphsBuffer[0];
            if (null === pGlyph || null === pGlyph.oBitmap) {continue;}

            this.fillGlyph(pGlyph, manager);
        }

        return this;
    },

	// Path methods

	beginPath: function () {
		this.ctx.beginPath();
		return this;
	},

	closePath: function () {
		this.ctx.closePath();
		return this;
	},

	moveTo: function (x, y) {
		var r = this._calcRect(x, y);
		this.ctx.moveTo(r.x, r.y);
		return this;
	},

	lineTo: function (x, y) {
		var r = this._calcRect(x, y);
		this.ctx.lineTo(r.x, r.y);
		return this;
	},

	lineDiag : function (x1, y1, x2, y2) {
		var isEven = 0 !== this.ctx.lineWidth % 2 ? 0.5 : 0;
		var r1 = this._calcRect(x1, y1);
		var r2 = this._calcRect(x2, y2);
		this.ctx.moveTo(r1.x + isEven, r1.y + isEven);
		this.ctx.lineTo(r2.x + isEven, r2.y + isEven);
		return this;
	},
	lineHor : function (x1, y, x2) {
		var isEven = 0 !== this.ctx.lineWidth % 2 ? 0.5 : 0;
		var r1 = this._calcRect(x1, y);
		var r2 = this._calcRect(x2, y);
		this.ctx.moveTo(r1.x, r1.y + isEven);
		this.ctx.lineTo(r2.x, r2.y + isEven);
		return this;
	},
	lineVer : function (x, y1, y2) {
		var isEven = 0 !== this.ctx.lineWidth % 2 ? 0.5 : 0;
		var r1 = this._calcRect(x, y1);
		var r2 = this._calcRect(x, y2);
		this.ctx.moveTo(r1.x + isEven, r1.y);
		this.ctx.lineTo(r2.x + isEven, r2.y);
		return this;
	},

	dashLineCleverHor : function (x1, y, x2) {
		var w_dot = c_oAscCoAuthoringDottedWidth, w_dist = c_oAscCoAuthoringDottedDistance;
		var _x1 = this._mct.transformPointX(x1, y);
		var _y  = this._mct.transformPointY(x1, y);
		var _x2 = this._mct.transformPointX(x2, y);
		var ctx = this.ctx;

		_x1 = (_x1 >> 0) + 0.5;
		_y  = (_y  >> 0) + 0.5;
		_x2 = (_x2 >> 0) + 0.5;

		for (; _x1 < _x2; _x1 += w_dist) {
			ctx.moveTo(_x1, _y);
			_x1 += w_dot;

			if (_x1 > _x2)
				_x1 = _x2;

			ctx.lineTo(_x1, _y);
		}
	},
	dashLineCleverVer : function (x, y1, y2) {
		var w_dot = c_oAscCoAuthoringDottedWidth, w_dist = c_oAscCoAuthoringDottedDistance;
		var _y1 = this._mct.transformPointY(x, y1);
		var _x  = this._mct.transformPointX(x, y1);
		var _y2 = this._mct.transformPointY(x, y2);
		var ctx = this.ctx;

		_y1 = (_y1 >> 0) + 0.5;
		_x  = (_x  >> 0) + 0.5;
		_y2 = (_y2 >> 0) + 0.5;

		for (; _y1 < _y2; _y1 += w_dist) {
			ctx.moveTo(_x, _y1);
			_y1 += w_dot;

			if (_y1 > _y2)
				_y1 = _y2;

			ctx.lineTo(_x, _y1);
		}
	},

	dashLine: function (x1, y1, x2, y2, w_dot, w_dist) {
		var len = Math.sqrt ((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1));
		if (len < 1)
			len = 1;

		var len_x1 = Math.abs(w_dot *(x2 - x1)/len);
		var len_y1 = Math.abs(w_dot *(y2 - y1)/len);
		var len_x2 = Math.abs(w_dist*(x2 - x1)/len);
		var len_y2 = Math.abs(w_dist*(y2 - y1)/len);
		var i, j;

		if (x1 <= x2 && y1 <= y2) {
			for (i = x1, j = y1; i < x2 || j < y2; i += len_x2, j += len_y2) {
				this.moveTo(i, j);

				i += len_x1;
				j += len_y1;

				if (i > x2)
					i = x2;
				if (j > y2)
					j = y2;

				this.lineTo(i, j);
			}
		} else if (x1 <= x2 && y1 > y2) {
			for (i = x1, j = y1; i < x2 || j > y2; i += len_x2, j -= len_y2) {
				this.moveTo(i, j);

				i += len_x1;
				j -= len_y1;

				if (i > x2)
					i = x2;
				if (j < y2)
					j = y2;

				this.lineTo(i, j);
			}
		} else if (x1 > x2 && y1 <= y2) {
			for (i = x1, j = y1; i > x2 || j < y2; i -= len_x2, j += len_y2) {
				this.moveTo(i, j);

				i -= len_x1;
				j += len_y1;

				if (i < x2)
					i = x2;
				if (j > y2)
					j = y2;

				this.lineTo(i, j);
			}
		} else {
			for (i = x1, j = y1; i > x2 || j > y2; i -= len_x2, j -= len_y2) {
				this.moveTo(i, j);

				i -= len_x1;
				j -= len_y1;

				if (i < x2)
					i = x2;
				if (j < y2)
					j = y2;

			this.lineTo(i, j);
			}
		}
	},

	dashRect: function (x1, y1, x2, y2, x3, y3, x4, y4, w_dot, w_dist) {
		this.dashLine(x1, y1, x2, y2, w_dot, w_dist);
		this.dashLine(x2, y2, x4, y4, w_dot, w_dist);
		this.dashLine(x4, y4, x3, y3, w_dot, w_dist);
		this.dashLine(x3, y3, x1, y1, w_dot, w_dist);
	},

	rect: function (x, y, w, h) {
		var r = this._calcRect(x, y, w, h);
		this.ctx.rect(r.x, r.y, r.w, r.h);
		return this;
	},

	arc: function (x, y, radius, startAngle, endAngle, antiClockwise, dx, dy) {
		var r = this._calcRect(x, y);
		dx = typeof dx !== "undefined" ? dx : 0;
		dy = typeof dy !== "undefined" ? dy : 0;
		this.ctx.arc(r.x + dx, r.y + dy, radius, startAngle, endAngle, antiClockwise);
		return this;
	},

	bezierCurveTo: function (x1, y1, x2, y2, x3, y3) {
		var p1 = this._calcRect(x1, y1),
		    p2 = this._calcRect(x2, y2),
		    p3 = this._calcRect(x3, y3);
		this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
		return this;
	},

	fill: function () {
		this.ctx.fill();
		return this;
	},

	stroke: function () {
		this.ctx.stroke();
		return this;
	},

	clip: function () {
		this.ctx.clip();
		return this;
	},

	// Image methods

	drawImage: function (img, sx, sy, sw, sh, dx, dy, dw, dh) {
		var sr = this._calcRect(sx, sy, sw, sh),
		    dr = this._calcRect(dx, dy, dw, dh);
		this.ctx.drawImage(img, sr.x, sr.y, sr.w, sr.h, dr.x, dr.y, dr.w, dr.h);
		return this;
	},

	// Private methods

	_calcRect: function (x, y, w, h) {
		var wh = w !== undefined && h !== undefined,
		    x2 = x + w - this._1px_x,
		    y2 = y + h - this._1px_y,
		    _x = this._mft.transformPointX(x, y),
		    _y = this._mft.transformPointY(x, y);
		return {
			x: asc_round(_x),
			y: asc_round(_y),
			w: wh ? asc_round(this._mft.transformPointX(x2, y2) - _x + 1) : undefined,
			h: wh ? asc_round(this._mft.transformPointY(x2, y2) - _y + 1) : undefined
		};
	},

	_calcMFT: function () {
		this._mft = this._mct.clone();
        this._mft.multiply(this._mbt, MATRIX_ORDER_PREPEND);
        this._mft.multiply(this._mt, MATRIX_ORDER_PREPEND);

        this._mift = this._mt.clone();
        this._mift.invert();
        this._mift.multiply(this._mft, MATRIX_ORDER_PREPEND);
	},

	/**
	 * @param {Number} w         Ширина текста
	 * @param {Number} wBB       Ширина Bound Box текста
	 * @param {CFontManager} fm  Объект CFontManager для получения метрик шрифта
	 * @param {Number} r         Коэффициент перевода pt -> в текущие единицы измерения (this.units)
	 * @return {TextMetrics}
	 */
	_calcTextMetrics: function (w, wBB, fm, r) {
		var factor = this.getFontSize() * r / fm.m_lUnits_Per_Em,
		    l = fm.m_lLineHeight * factor,
		    b = fm.m_lAscender * factor,
		    d = Math.abs(fm.m_lDescender * factor);
		return new TextMetrics(w, b + d, l, b, d, this.font.FontSize, 0, wBB);
	}
};


/*
 * Export
 * -----------------------------------------------------------------------------
 */

window["Asc"].getCvtRatio      = getCvtRatio;
window["Asc"].calcNearestPt    = calcNearestPt;
window["Asc"].colorObjToAscColor = colorObjToAscColor;

window["Asc"].FontProperties   = FontProperties;
window["Asc"].TextMetrics      = TextMetrics;
window["Asc"].FontMetrics      = FontMetrics;
window["Asc"].DrawingContext   = DrawingContext;
window["Asc"].Matrix           = Matrix;

})(jQuery, window);
