import TextInlines from './TextInlines';
import StyleContextStack from './StyleContextStack';
import ColumnCalculator from './columnCalculator';
import { defaultTableLayout } from './tableLayouts';
import { isString, isNumber, isObject } from './helpers/variableType';
import { stringifyNode, getNodeId, getNodeMargin } from './helpers/node';
import { pack } from './helpers/tools';
import qrEncoder from './qrEnc.js';

class DocMeasure {
	constructor(
		pdfDocument,
		styleDictionary,
		defaultStyle,
		svgMeasure,
		tableLayouts
	) {
		this.pdfDocument = pdfDocument;
		this.textInlines = new TextInlines(pdfDocument);
		this.styleStack = new StyleContextStack(styleDictionary, defaultStyle);
		this.svgMeasure = svgMeasure;
		this.tableLayouts = tableLayouts;
		this.autoImageIndex = 1;
	}

	/**
	 * Measures all nodes and sets min/max-width properties required for the second
	 * layout-pass.
	 *
	 * @param {object} docStructure document-definition-object
	 * @returns {object} document-measurement-object
	 */
	measureDocument(docStructure) {
		return this.measureNode(docStructure);
	}

	measureBlock(node) {
		return this.measureNode(node);
	}

	measureNode(node) {
		return this.styleStack.auto(node, () => {
			// TODO: refactor + rethink whether this is the proper way to handle margins
			node._margin = getNodeMargin(node, this.styleStack);

			if (node.section) {
				return extendMargins(this.measureSection(node));
			} else if (node.columns) {
				return extendMargins(this.measureColumns(node));
			} else if (node.stack) {
				return extendMargins(this.measureVerticalContainer(node));
			} else if (node.ul) {
				return extendMargins(this.measureUnorderedList(node));
			} else if (node.ol) {
				return extendMargins(this.measureOrderedList(node));
			} else if (node.table) {
				return extendMargins(this.measureTable(node));
			} else if (node.text !== undefined) {
				return extendMargins(this.measureLeaf(node));
			} else if (node.toc) {
				return extendMargins(this.measureToc(node));
			} else if (node.image) {
				return extendMargins(this.measureImage(node));
			} else if (node.svg) {
				return extendMargins(this.measureSVG(node));
			} else if (node.canvas) {
				return extendMargins(this.measureCanvas(node));
			} else if (node.qr) {
				return extendMargins(this.measureQr(node));
			} else if (node.attachment) {
				return extendMargins(this.measureAttachment(node));
			} else {
				throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
			}
		});

		function extendMargins(node) {
			let margin = node._margin;

			if (margin) {
				node._minWidth += margin[0] + margin[2];
				node._maxWidth += margin[0] + margin[2];
			}

			return node;
		}
	}

	measureImageWithDimensions(node, dimensions) {
		if (node.fit) {
			let factor = (dimensions.width / dimensions.height > node.fit[0] / node.fit[1]) ? node.fit[0] / dimensions.width : node.fit[1] / dimensions.height;
			node._width = node._minWidth = node._maxWidth = dimensions.width * factor;
			node._height = dimensions.height * factor;
		} else if (node.cover) {
			node._width = node._minWidth = node._maxWidth = node.cover.width;
			node._height = node._minHeight = node._maxHeight = node.cover.height;
		} else {
			let nodeWidth = isNumber(node.width) ? node.width : undefined;
			let nodeHeight = isNumber(node.height) ? node.height : undefined;
			let ratio = dimensions.width / dimensions.height;

			node._width = node._minWidth = node._maxWidth = nodeWidth || (nodeHeight ? (nodeHeight * ratio) : dimensions.width);
			node._height = nodeHeight || (nodeWidth ? nodeWidth / ratio : dimensions.height);

			if (isNumber(node.maxWidth) && node.maxWidth < node._width) {
				node._width = node._minWidth = node._maxWidth = node.maxWidth;
				node._height = node._width * dimensions.height / dimensions.width;
			}

			if (isNumber(node.maxHeight) && node.maxHeight < node._height) {
				node._height = node.maxHeight;
				node._width = node._minWidth = node._maxWidth = node._height * dimensions.width / dimensions.height;
			}

			if (isNumber(node.minWidth) && node.minWidth > node._width) {
				node._width = node._minWidth = node._maxWidth = node.minWidth;
				node._height = node._width * dimensions.height / dimensions.width;
			}

			if (isNumber(node.minHeight) && node.minHeight > node._height) {
				node._height = node.minHeight;
				node._width = node._minWidth = node._maxWidth = node._height * dimensions.width / dimensions.height;
			}
		}

		node._alignment = this.styleStack.getProperty('alignment');
	}

	convertIfBase64Image(node) {
		if (/^data:image\/(jpeg|jpg|png);base64,/.test(node.image)) { // base64 image
			let label = `$$pdfmake$$${this.autoImageIndex++}`;
			this.pdfDocument.images[label] = node.image;
			node.image = label;
		}
	}

	measureImage(node) {
		this.convertIfBase64Image(node);

		let image = this.pdfDocument.provideImage(node.image);

		let imageSize = { width: image.width, height: image.height };

		// If EXIF orientation calls for it, swap width and height
		if (image.orientation > 4) {
			imageSize = { width: image.height, height: image.width };
		}

		this.measureImageWithDimensions(node, imageSize);

		return node;
	}

	measureSVG(node) {
		let dimensions = this.svgMeasure.measureSVG(node.svg);

		this.measureImageWithDimensions(node, dimensions);

		node.font = this.styleStack.getProperty('font');

		// SVG requires a defined width and height
		if (!isNumber(node._width) && !isNumber(node._height)) {
			throw new Error('SVG is missing defined width and height.');
		} else if (!isNumber(node._width)) {
			throw new Error('SVG is missing defined width.');
		} else if (!isNumber(node._height)) {
			throw new Error('SVG is missing defined height.');
		}

		// scale SVG based on final dimension
		node.svg = this.svgMeasure.writeDimensions(node.svg, { width: node._width, height: node._height });

		return node;
	}

	measureLeaf(node) {

		if (node._textRef && node._textRef._textNodeRef.text) {
			node.text = node._textRef._textNodeRef.text;
		}

		// Make sure style properties of the node itself are considered when building inlines.
		// We could also just pass [node] to buildInlines, but that fails for bullet points.
		let styleStack = this.styleStack.clone();
		styleStack.push(node);

		let data = this.textInlines.buildInlines(node.text, styleStack);

		node._inlines = data.items;
		node._minWidth = data.minWidth;
		node._maxWidth = data.maxWidth;

		return node;
	}

	measureToc(node) {
		if (node.toc.title) {
			node.toc.title = this.measureNode(node.toc.title);
		}

		if (node.toc._items.length > 0) {
			let body = [];
			let textStyle = node.toc.textStyle || {};
			let numberStyle = node.toc.numberStyle || textStyle;
			let textMargin = node.toc.textMargin || [0, 0, 0, 0];

			if (node.toc.sortBy === 'title') {
				node.toc._items.sort((a, b) => {
					return a._textNodeRef.text.localeCompare(b._textNodeRef.text, node.toc.sortLocale);
				});
			}

			for (let i = 0, l = node.toc._items.length; i < l; i++) {
				let item = node.toc._items[i];
				let lineStyle = item._textNodeRef.tocStyle || textStyle;
				let lineMargin = item._textNodeRef.tocMargin || textMargin;
				let lineNumberStyle = item._textNodeRef.tocNumberStyle || numberStyle;
				let destination = getNodeId(item._nodeRef);
				body.push([
					{ text: item._textNodeRef.text, linkToDestination: destination, alignment: 'left', style: lineStyle, margin: lineMargin },
					{ text: '00000', linkToDestination: destination, alignment: 'right', _tocItemRef: item._nodeRef, style: lineNumberStyle, margin: [0, lineMargin[1], 0, lineMargin[3]] }
				]);

				if (node.toc.outlines) {
					item._textNodeRef.outline = item._textNodeRef.outline || true;
				}
			}

			node.toc._table = {
				table: {
					dontBreakRows: true,
					widths: ['*', 'auto'],
					body: body
				},
				layout: 'noBorders'
			};

			node.toc._table = this.measureNode(node.toc._table);
		}

		return node;
	}

	measureVerticalContainer(node) {
		let items = node.stack;

		node._minWidth = 0;
		node._maxWidth = 0;

		for (let i = 0, l = items.length; i < l; i++) {
			items[i] = this.measureNode(items[i]);

			node._minWidth = Math.max(node._minWidth, items[i]._minWidth);
			node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth);
		}

		return node;
	}

	gapSizeForList() {
		return this.textInlines.sizeOfText('9. ', this.styleStack);
	}

	buildUnorderedMarker(item, styleStack, gapSize, type) {
		function buildDisc(gapSize, color) {
			// TODO: ascender-based calculations
			let radius = gapSize.fontSize / 6;
			return {
				canvas: [{
					x: radius,
					y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - gapSize.fontSize / 3,
					r1: radius,
					r2: radius,
					type: 'ellipse',
					color: color
				}]
			};
		}

		function buildSquare(gapSize, color) {
			// TODO: ascender-based calculations
			let size = gapSize.fontSize / 3;
			return {
				canvas: [{
					x: 0,
					y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - (gapSize.fontSize / 3) - (size / 2),
					h: size,
					w: size,
					type: 'rect',
					color: color
				}]
			};
		}

		function buildCircle(gapSize, color) {
			// TODO: ascender-based calculations
			let radius = gapSize.fontSize / 6;
			return {
				canvas: [{
					x: radius,
					y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - gapSize.fontSize / 3,
					r1: radius,
					r2: radius,
					type: 'ellipse',
					lineColor: color
				}]
			};
		}

		let marker;
		let color = StyleContextStack.getStyleProperty(item, styleStack, 'markerColor', undefined) || styleStack.getProperty('color') || 'black';

		switch (type) {
			case 'circle':
				marker = buildCircle(gapSize, color);
				break;

			case 'square':
				marker = buildSquare(gapSize, color);
				break;

			case 'none':
				marker = {};
				break;

			case 'disc':
			default:
				marker = buildDisc(gapSize, color);
				break;
		}

		marker._minWidth = marker._maxWidth = gapSize.width;
		marker._minHeight = marker._maxHeight = gapSize.height;

		return marker;
	}

	buildOrderedMarker(item, counter, styleStack, type, separator) {
		function prepareAlpha(counter) {
			function toAlpha(num) {
				return (num >= 26 ? toAlpha((num / 26 >> 0) - 1) : '') + 'abcdefghijklmnopqrstuvwxyz'[num % 26 >> 0];
			}

			if (counter < 1) {
				return counter.toString();
			}

			return toAlpha(counter - 1);
		}

		function prepareRoman(counter) {
			if (counter < 1 || counter > 4999) {
				return counter.toString();
			}
			let num = counter;
			let lookup = { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 };
			let roman = '';
			for (let i in lookup) {
				while (num >= lookup[i]) {
					roman += i;
					num -= lookup[i];
				}
			}
			return roman;
		}

		function prepareDecimal(counter) {
			return counter.toString();
		}

		let counterText;
		switch (type) {
			case 'none':
				counterText = null;
				break;

			case 'upper-alpha':
				counterText = prepareAlpha(counter).toUpperCase();
				break;

			case 'lower-alpha':
				counterText = prepareAlpha(counter);
				break;

			case 'upper-roman':
				counterText = prepareRoman(counter);
				break;

			case 'lower-roman':
				counterText = prepareRoman(counter).toLowerCase();
				break;

			case 'decimal':
			default:
				counterText = prepareDecimal(counter);
				break;
		}

		if (counterText === null) {
			return {};
		}

		if (separator) {
			if (Array.isArray(separator)) {
				if (separator[0]) {
					counterText = separator[0] + counterText;
				}

				if (separator[1]) {
					counterText += separator[1];
				}
				counterText += ' ';
			} else {
				counterText += `${separator} `;
			}
		}

		let markerColor = StyleContextStack.getStyleProperty(item, styleStack, 'markerColor', undefined) || styleStack.getProperty('color') || 'black';
		let textArray = {
			text: counterText,
			color: markerColor
		};

		return { _inlines: this.textInlines.buildInlines(textArray, styleStack).items };
	}

	measureUnorderedList(node) {
		let style = this.styleStack.clone();
		let items = node.ul;
		node.type = node.type || 'disc';
		node._gapSize = this.gapSizeForList();
		node._minWidth = 0;
		node._maxWidth = 0;

		for (let i = 0, l = items.length; i < l; i++) {
			let item = items[i] = this.measureNode(items[i]);

			if (!item.ol && !item.ul) {
				item.listMarker = this.buildUnorderedMarker(item, style, node._gapSize, item.listType || node.type);
			}

			node._minWidth = Math.max(node._minWidth, items[i]._minWidth + node._gapSize.width);
			node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth + node._gapSize.width);
		}

		return node;
	}

	measureOrderedList(node) {
		let style = this.styleStack.clone();
		let items = node.ol;
		node.type = node.type || 'decimal';
		node.separator = node.separator || '.';
		node.reversed = node.reversed || false;
		if (!isNumber(node.start)) {
			node.start = node.reversed ? items.length : 1;
		}
		node._gapSize = this.gapSizeForList();
		node._minWidth = 0;
		node._maxWidth = 0;

		let counter = node.start;
		for (let i = 0, l = items.length; i < l; i++) {
			let item = items[i] = this.measureNode(items[i]);

			if (!item.ol && !item.ul) {
				let counterValue = isNumber(item.counter) ? item.counter : counter;
				item.listMarker = this.buildOrderedMarker(item, counterValue, style, item.listType || node.type, node.separator);
				if (item.listMarker._inlines) {
					node._gapSize.width = Math.max(node._gapSize.width, item.listMarker._inlines[0].width);
				}

				if (node.reversed) {
					counter--;
				} else {
					counter++;
				}
			}

			node._minWidth = Math.max(node._minWidth, items[i]._minWidth);
			node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth);

		}

		node._minWidth += node._gapSize.width;
		node._maxWidth += node._gapSize.width;

		for (let i = 0, l = items.length; i < l; i++) {
			let item = items[i];
			if (!item.ol && !item.ul) {
				item.listMarker._minWidth = item.listMarker._maxWidth = node._gapSize.width;
			}
		}

		return node;
	}

	measureSection(node) {
		// TODO: properties

		node.section = this.measureNode(node.section);

		return node;
	}

	measureColumns(node) {
		let columns = node.columns;
		node._gap = this.styleStack.getProperty('columnGap') || 0;

		for (let i = 0, l = columns.length; i < l; i++) {
			columns[i] = this.measureNode(columns[i]);
		}

		let measures = ColumnCalculator.measureMinMax(columns);

		let numGaps = (columns.length > 0) ? (columns.length - 1) : 0;
		node._minWidth = measures.min + node._gap * numGaps;
		node._maxWidth = measures.max + node._gap * numGaps;

		return node;
	}

	measureTable(node) {
		extendTableWidths(node);
		node._layout = getLayout(this.tableLayouts);
		node._offsets = getOffsets(node._layout);

		let colSpans = [];
		let col;
		let row;
		let cols;
		let rows;

		for (col = 0, cols = node.table.body[0].length; col < cols; col++) {
			let c = node.table.widths[col];
			c._minWidth = 0;
			c._maxWidth = 0;

			for (row = 0, rows = node.table.body.length; row < rows; row++) {
				let rowData = node.table.body[row];
				let data = rowData[col];
				if (data === undefined) {
					throw new Error(`Malformed table row, a cell is undefined.\nRow index: ${row}\nColumn index: ${col}\nRow data: ${stringifyNode(rowData)}`);
				}
				if (data === null) { // transform to object
					data = '';
				}

				if (!data._span) {
					data = rowData[col] = this.styleStack.auto(data, measureCb(this, data));

					if (data.colSpan && data.colSpan > 1) {
						markSpans(rowData, col, data.colSpan);
						colSpans.push({ col: col, span: data.colSpan, minWidth: data._minWidth, maxWidth: data._maxWidth });
					} else {
						c._minWidth = Math.max(c._minWidth, data._minWidth);
						c._maxWidth = Math.max(c._maxWidth, data._maxWidth);
					}
				}

				if (data.rowSpan && data.rowSpan > 1) {
					markVSpans(node.table, row, col, data.rowSpan);
				}
			}
		}

		extendWidthsForColSpans();

		let measures = ColumnCalculator.measureMinMax(node.table.widths);

		node._minWidth = measures.min + node._offsets.total;
		node._maxWidth = measures.max + node._offsets.total;

		return node;

		function measureCb(_this, data) {
			return () => {
				if (isObject(data)) {
					data.fillColor = _this.styleStack.getProperty('fillColor');
					data.fillOpacity = _this.styleStack.getProperty('fillOpacity');
				}
				return _this.measureNode(data);
			};
		}

		function getLayout(tableLayouts) {
			let layout = node.layout;

			if (isString(layout)) {
				layout = tableLayouts[layout];
			}

			return pack(defaultTableLayout, layout);
		}

		function getOffsets(layout) {
			let offsets = [];
			let totalOffset = 0;
			let prevRightPadding = 0;

			for (let i = 0, l = node.table.widths.length; i < l; i++) {
				let lOffset = prevRightPadding + layout.vLineWidth(i, node) + layout.paddingLeft(i, node);
				offsets.push(lOffset);
				totalOffset += lOffset;
				prevRightPadding = layout.paddingRight(i, node);
			}

			totalOffset += prevRightPadding + layout.vLineWidth(node.table.widths.length, node);

			return {
				total: totalOffset,
				offsets: offsets
			};
		}

		function extendWidthsForColSpans() {
			let q;
			let j;

			for (let i = 0, l = colSpans.length; i < l; i++) {
				let span = colSpans[i];

				let currentMinMax = getMinMax(span.col, span.span, node._offsets);
				let minDifference = span.minWidth - currentMinMax.minWidth;
				let maxDifference = span.maxWidth - currentMinMax.maxWidth;

				if (minDifference > 0) {
					q = minDifference / span.span;

					for (j = 0; j < span.span; j++) {
						node.table.widths[span.col + j]._minWidth += q;
					}
				}

				if (maxDifference > 0) {
					q = maxDifference / span.span;

					for (j = 0; j < span.span; j++) {
						node.table.widths[span.col + j]._maxWidth += q;
					}
				}
			}
		}

		function getMinMax(col, span, offsets) {
			let result = { minWidth: 0, maxWidth: 0 };

			for (let i = 0; i < span; i++) {
				result.minWidth += node.table.widths[col + i]._minWidth + (i ? offsets.offsets[col + i] : 0);
				result.maxWidth += node.table.widths[col + i]._maxWidth + (i ? offsets.offsets[col + i] : 0);
			}

			return result;
		}

		function markSpans(rowData, col, span) {
			for (let i = 1; i < span; i++) {
				rowData[col + i] = {
					_span: true,
					_minWidth: 0,
					_maxWidth: 0,
					rowSpan: rowData[col].rowSpan
				};
			}
		}

		function markVSpans(table, row, col, span) {
			for (let i = 1; i < span; i++) {
				table.body[row + i][col] = {
					_span: true,
					_minWidth: 0,
					_maxWidth: 0,
					fillColor: table.body[row][col].fillColor,
					fillOpacity: table.body[row][col].fillOpacity
				};
			}
		}

		function extendTableWidths(node) {
			if (!node.table.widths) {
				node.table.widths = 'auto';
			}

			if (isString(node.table.widths)) {
				node.table.widths = [node.table.widths];

				while (node.table.widths.length < node.table.body[0].length) {
					node.table.widths.push(node.table.widths[node.table.widths.length - 1]);
				}
			}

			for (let i = 0, l = node.table.widths.length; i < l; i++) {
				let w = node.table.widths[i];
				if (isNumber(w) || isString(w)) {
					node.table.widths[i] = { width: w };
				}
			}
		}
	}

	measureCanvas(node) {
		let w = 0;
		let h = 0;

		for (let i = 0, l = node.canvas.length; i < l; i++) {
			let vector = node.canvas[i];

			switch (vector.type) {
				case 'ellipse':
					w = Math.max(w, vector.x + vector.r1);
					h = Math.max(h, vector.y + vector.r2);
					break;
				case 'rect':
					w = Math.max(w, vector.x + vector.w);
					h = Math.max(h, vector.y + vector.h);
					break;
				case 'line':
					w = Math.max(w, vector.x1, vector.x2);
					h = Math.max(h, vector.y1, vector.y2);
					break;
				case 'polyline':
					for (let i2 = 0, l2 = vector.points.length; i2 < l2; i2++) {
						w = Math.max(w, vector.points[i2].x);
						h = Math.max(h, vector.points[i2].y);
					}
					break;
			}
		}

		node._minWidth = node._maxWidth = w;
		node._minHeight = node._maxHeight = h;
		node._alignment = this.styleStack.getProperty('alignment');

		return node;
	}

	measureQr(node) {
		node = qrEncoder.measure(node);
		node._alignment = this.styleStack.getProperty('alignment');
		return node;
	}

	measureAttachment(node) {
		node._width = node.width || 7;
		node._height = node.height || 18;

		return node;
	}
}

export default DocMeasure;
