var Class = require('../../core/class');
var Color = require('../../core/color');
var Mode = require('../../modes/current');
var Rectangle = require('../../shapes/rectangle');

// Regular Expressions

var matchURL = /^\s*url\(["'\s]*([^\)]*?)["'\s]*\)/,
	requiredNumber = '(?:\\s+|\\s*,\\s*)([^\\s,\\)]+)';
	number = '(?:' + requiredNumber + ')?',
	matchViewBox = new RegExp('^\\s*([^\\s,]+)' + requiredNumber + requiredNumber + requiredNumber),
	matchUnit = /^\s*([\+\-\d\.]+(?:e\d+)?)(|px|em|ex|in|pt|pc|mm|cm|%)\s*$/i;

// Environment Settings
var dpi = 72, emToEx = 0.5;

var styleSheet = function(){},
	defaultStyles = {
		'viewportWidth': 500,
		'viewportHeight': 500,
		'font-family': 'Arial',
		'font-size': 12,
		'color': 'black',
		'fill': 'black'
	},
	nonInheritedStyles = {
		'stop-color': 'black',
		'stop-opacity': 1,
		'clip-path': null,
		'filter': null,
		'mask': null,
		'opacity': 1,
		'cursor': null
	};

// Visitor

var SVGParser = Class({

	initialize: function(mode){
		this.MODE = mode;
	},

	// TODO Fix this silly API
	parseAsSurface: function(element, styles){
		return this.parse(element, styles, true);
	},

	parse: function(element, styles, asSurface){
		if (typeof element == 'string') element = this.parseXML(element);

		if (!styles)
			styles = this.findStyles(element);
		else
			for (var style in defaultStyles)
				if (!(style in styles))
					styles[style] = defaultStyles[style];

		if (element.documentElement || asSurface){
			element = element.documentElement || element;
			var canvas = new this.MODE.Surface(
				this.parseLength(element.getAttribute('width') || '100%', styles, 'x'),
				this.parseLength(element.getAttribute('height') || '100%', styles, 'y')
			);
			if (element.getAttribute('viewBox'))
				canvas.grab(this.parse(element, styles));
			else
				this.container(element, this.parseStyles(element, styles), canvas);
			return canvas;
		}
		if (element.nodeType != 1 || element.getAttribute('requiredExtensions') || element.getAttribute('systemLanguage') != null) return null;
		styles = this.parseStyles(element, styles);
		var parseFunction = this[element.nodeName + 'Element'];
		return parseFunction ? parseFunction.call(this, element, styles) : null;
	},
	
	parseXML: window.DOMParser ? function(text){
		return new DOMParser().parseFromString(text, 'text/xml');
	} : function(text){
		try {
			var xml;
			try { xml = new ActiveXObject('MSXML2.DOMDocument'); }
			catch (e){ xml = new ActiveXObject('Microsoft.XMLDOM'); }
			xml.resolveExternals = false;
			xml.validateOnParse = false;
			xml.async = false;
			xml.preserveWhiteSpace = true;
			xml.loadXML(text);
			return xml;
		} catch (e){
			return null;
		}
	},
	
	parseStyles: function(element, styles){
		styleSheet.prototype = styles;
		var newSheet = new styleSheet();
		for (var key in nonInheritedStyles) newSheet[key] = nonInheritedStyles[key];
		this.applyStyles(element, newSheet);
		if (newSheet.hasOwnProperty('font-size')){
			var newFontSize = this.parseLength(newSheet['font-size'], styles, 'font');
			if (newFontSize != null) newSheet['font-size'] = newFontSize;
		}
		if (newSheet.hasOwnProperty('text-decoration')){
			newSheet['text-decoration-color'] = newSheet.color;
		}
		return newSheet;
	},
	
	findStyles: function(element){
		if (!element || element.nodeType != 1) return defaultStyles;
		var styles = this.findStyles(element.parentNode);
		return this.parseStyles(element, styles);
	},
	
	applyStyles: function(element, target){
		var attributes = element.attributes;
		for (var i = 0, l = attributes.length; i < l; i++){
			var attribute = attributes[i],
			    name = attribute.nodeName,
			    value = attribute.nodeValue;
			if (value != 'inherit'){
				target[name] = value;
				if (name == 'fill') target['fill_document'] = element.ownerDocument;
			}
		}
		return target;
	},

	findById: function(document, id){
		// if (document.getElementById) return document.getElementById(id); Not reliable
		if (this.cacheDocument != document){
			this.ids = {};
			this.lastSweep = null;
			this.cacheDocument = document;
		}
		var ids = this.ids;
		if (ids[id] != null) return ids[id];
		var root = document.documentElement, node = this.lastSweep || root;
		treewalker: while (node){
			if (node.nodeType == 1){
				var newID = node.getAttribute('id') || node.getAttribute('xml:id');
				if (newID && ids[newID] == null) ids[newID] = node;
				if (newID == id){
					this.lastSweep = node;
					return node;
				}
			}
			if (node.firstChild){
				node = node.firstChild;
			} else {
				while (!node.nextSibling){
					node = node.parentNode;
					if (!node || node == root) break treewalker;
				}
				node = node.nextSibling;
			}
		}
		return null;
	},
	
	findByURL: function(document, url, callback){
		callback.call(this, url && url[0] == '#' ? this.findById(document, url.substr(1)) : null);
	},

	resolveURL: function(url){
		return url;
	},
	
	parseLength: function(value, styles, dimension){
		var match = matchUnit.exec(value);
		if (!match) return null;
		var result = parseFloat(match[1]);
		switch(match[2]){
			case '': case 'px': return result;
			case 'em': return result * styles['font-size'];
			case 'ex': return result * styles['font-size'] * emToEx;
			case 'in': return result * dpi;
			case 'pt': return result * dpi / 72;
			case 'pc': return result * dpi / 6;
			case 'mm': return result * dpi / 25.4;
			case 'cm': return result * dpi / 2.54;
			case '%':
				var w = styles.viewportWidth, h = styles.viewportHeight;
				if (dimension == 'font') return result * styles['font-size'] / 100;
				if (dimension == 'x') return result * w / 100;
				if (dimension == 'y') return result * h / 100;
				return result * Math.sqrt(w * w + h * h) / Math.sqrt(2) / 100;
		}
	},
	
	parseColor: function(value, opacity, styles){
		if (value == 'currentColor') value = styles.color;
		try {
			var color = new Color(value);
		} catch (x){
			// Ignore unparsable colors, TODO: log
			return null;
		}
		color.alpha = opacity == null ? 1 : +opacity;
		return color;
	},
	
	getLengthAttribute: function(element, styles, attr, dimension){
		return this.parseLength(element.getAttribute(attr) || 0, styles, dimension);
	},
	
	container: function(element, styles, container){
		if (container.width != null) styles.viewportWidth = container.width;
		if (container.height != null) styles.viewportHeight = container.height;
		this.filter(styles, container);
		this.describe(element, styles, container);
		var node = element.firstChild;
		while (node){
			var art = this.parse(node, styles);
			if (art) container.grab(art);
			node = node.nextSibling;
		}
		return container;
	},

	shape: function(element, styles, target, x, y){
		this.transform(element, target);
		target.transform(1, 0, 0, 1, x, y);
		this.fill(styles, target, x, y);
		this.stroke(styles, target);
		this.filter(styles, target);
		if (styles.visibility == 'hidden') target.hide();
		this.describe(element, styles, target);
		return target;
	},
	
	fill: function(styles, target, x, y){
		if (!styles.fill || styles.fill == 'none') return;
		var match;
		if (match = matchURL.exec(styles.fill)){
			this.findByURL(styles.fill_document, match[1], function(fill){
				var fillFunction = fill && this[fill.nodeName + 'Fill'];
				if (fillFunction) fillFunction.call(this, fill, this.findStyles(fill), target, x, y);
			});
		} else {
			target.fill(this.parseColor(styles.fill, styles['fill-opacity'], styles));
		}
	},
	
	stroke: function(styles, target){
		if (!styles.stroke || styles.stroke == 'none' || matchURL.test(styles.stroke)) return; // Advanced stroke colors are not supported, TODO: log
		var color = this.parseColor(styles.stroke, styles['stroke-opacity'], styles),
			width = this.parseLength(styles['stroke-width'], styles),
			cap = styles['stroke-linecap'] || 'butt',
			join = styles['stroke-linejoin'] || 'miter';
		target.stroke(color, width == null ? 1 : width, cap, join);
	},

	filter: function(styles, target){
		if (styles.opacity != 1 && target.blend) target.blend(styles.opacity);
		if (styles.display == 'none') target.hide();
	},
	
	describe: function(element, styles, target){
		var node = element.firstChild, title = '';
		if (element.nodeName != 'svg')
			while (node){
				if (node.nodeName == 'title') title += node.firstChild && node.firstChild.nodeValue;
				node = node.nextSibling;
			}
		if (styles.cursor || title) target.indicate(styles.cursor, title);
	},

	transform: function(element, target){
		var transform = element.getAttribute('transform'), match;
		var matchTransform = new RegExp('([a-z]+)\\s*\\(\\s*([^\\s,\\)]+)' + number + number + number + number + number + '\\s*\\)', 'gi');
		while(match = transform && matchTransform.exec(transform)){
			switch(match[1]){
				case 'matrix':
					target.transform(match[2], match[3], match[4], match[5], match[6], match[7]);
					break;
				case 'translate':
					target.transform(1, 0, 0, 1, match[2], match[3]);
					break;
				case 'scale':
					target.transform(match[2], 0, 0, match[3] || match[2]);
					break;
				case 'rotate':
					var rad = match[2] * Math.PI / 180, cos = Math.cos(rad), sin = Math.sin(rad);
					target.transform(1, 0, 0, 1, match[3], match[4])
						.transform(cos, sin, -sin, cos)
						.transform(1, 0, 0, 1, -match[3], -match[4]);
					break;
				case 'skewX':
					target.transform(1, 0, Math.tan(match[2] * Math.PI / 180), 1);
					break;
				case 'skewY':
					target.transform(1, Math.tan(match[2] * Math.PI / 180), 0, 1);
					break;
			}
		}
	},
	
	svgElement: function(element, styles){
		var viewbox = element.getAttribute('viewBox'),
		    match = matchViewBox.exec(viewbox),
		    x = this.getLengthAttribute(element, styles, 'x', 'x'),
		    y = this.getLengthAttribute(element, styles, 'y', 'y'),
		    width = this.getLengthAttribute(element, styles, 'width', 'x'),
		    height = this.getLengthAttribute(element, styles, 'height', 'y'),
		    group = match ? new this.MODE.Group(+match[3], +match[4]) : new this.MODE.Group(width || null, height || null);
		if (width && height) group.resizeTo(width, height); // TODO: Aspect ratio
		if (match) group.transform(1, 0, 0, 1, -match[1], -match[2]);
		this.container(element, styles, group);
		group.move(x, y);
		return group;
	},
	
	gElement: function(element, styles){
		var group = new this.MODE.Group();
		this.transform(element, group);
		this.container(element, styles, group);
		return group;
	},
	
	useElement: function(element, styles){
		var placeholder = new this.MODE.Group(),
		    x = this.getLengthAttribute(element, styles, 'x', 'x'),
		    y = this.getLengthAttribute(element, styles, 'y', 'y'),
		    width = this.getLengthAttribute(element, styles, 'width', 'x'),
		    height = this.getLengthAttribute(element, styles, 'height', 'y');
		
		this.transform(element, placeholder);
		placeholder.transform(1, 0, 0, 1, x, y);
		
		this.findByURL(element.ownerDocument, element.getAttribute('xlink:href') || element.getAttribute('href'), function(target){
			if (!target || target.nodeType != 1) return;
			
			var parseFunction = target.nodeName == 'symbol' ? this.svgElement : this[target.nodeName + 'Element'];
			if (!parseFunction) return;
			
			styles = this.parseStyles(element, this.parseStyles(target, styles));
			
			var symbol = parseFunction.call(this, target, styles);
			if (!symbol) return;
			if (width && height) symbol.resizeTo(width, height); // TODO: Aspect ratio, maybe resize the placeholder instead
			placeholder.grab(symbol);
		});
		
		return placeholder;
	},
	
	switchElement: function(element, styles){
		var node = element.firstChild;
		while (node){
			var art = this.parse(node, styles);
			if (art) return art;
			node = node.nextSibling;
		}
		return null;
	},
	
	aElement: function(element, styles){
		// For now treat it like a group
		return this.gElement(element, styles);
	},
	
	pathElement: function(element, styles){
		var shape = new this.MODE.Shape(element.getAttribute('d') || null);
		this.shape(element, styles, shape);
		return shape;
	},
	
	imageElement: function(element, styles){
		var href = this.resolveURL(element.getAttribute('xlink:href') || element.getAttribute('href')),
		    width = this.getLengthAttribute(element, styles, 'width', 'x'),
		    height = this.getLengthAttribute(element, styles, 'height', 'y'),
		    x = this.getLengthAttribute(element, styles, 'x', 'x'),
		    y = this.getLengthAttribute(element, styles, 'y', 'y'),
		    clipPath = element.getAttribute('clip-path'),
		    image,
		    match;
		
		if (clipPath && (match = matchURL.exec(clipPath)) && match[1][0] == '#'){
			var clip = this.findById(element.ownerDocument, match[1].substr(1));
			if (clip){
				image = this.switchElement(clip, styles);
				if (image){
					if (typeof image.fillImage == 'function'){
						image.fillImage(href, width, height);
						if (image.stroke) image.stroke(0);
					} else {
						image = null;
					}
				}
			}
		}
		if (!image){
			//image = new Image(href, width, height); TODO
			image = new Rectangle(width, height).fillImage(href, width, height);
		}
		this.filter(styles, image);
		if (styles.visibility == 'hidden') target.hide();
		this.describe(element, styles, image);
		this.transform(element, image);
		image.transform(1, 0, 0, 1, x, y);
		return image;
	}

});

SVGParser.parse = function(element, styles){
	return new SVGParser(Mode).parse(element, styles);
};

SVGParser.implement = function(obj){
	for (var key in obj)
		this.prototype[key] = obj[key];
};

module.exports = SVGParser;