#!/usr/bin/env node

/* jshint node: true */

( function () {
	var i, count, currentClassName, group, symbol, symbols, symbolObject,
		symbolsData, cssData, cssLines, alignBaseline,
		unmodifiedClasses = {},
		cssRules = [], // Whole CSS rules
		cssClasses = {}, // Unique part of class name and whether baseline is shifted
		currentRule = [],
		symbolList = [], // Symbols whose CSS rules need to be added or adjusted
		symbolsFile = '../symbols.json',
		cssFile = '../ve.ui.MWMathSymbols.css',
		cssPrefix = '.ve-ui-mwMathSymbol-',
		fs = require( 'fs' ),
		http = require( 'http' ),
		querystring = require( 'querystring' ),
		mathoidMaxConnections = 20,
		// If symbol.alignBaseline is true, a background-position property will be added to the
		// CSS rule to shift the baseline of the SVG to be a certain proportion of the way up the
		// button.
		singleButtonHeight = 1.8, // Height of the single-height math dialog buttons in em
		baseline = 0.65; // Proportion of the way down the button the baseline should be

	symbolsData = fs.readFileSync( symbolsFile ).toString();
	try {
		cssData = fs.readFileSync( cssFile ).toString();
	} catch ( e ) {}

	function encodeURIComponentForCSS( str ) {
		return encodeURIComponent( str )
			.replace( /[!'\(\)\*]/g, function ( chr ) {
				return '%' + chr.charCodeAt( 0 ).toString( 16 );
			} );
	}

	/**
	 * Make the className, replacing any non-alphanumerics with their character code
	 *
	 * The reverse of function would look like this, although we have no use for it yet:
	 *
	 *  return className.replace( /_([0-9]+)_/g, function () {
	 *  	return String.fromCharCode( +arguments[ 1 ] );
	 *  } );
	 */
	function texToClass( tex ) {
		return tex.replace( /[^\w]/g, function ( c ) {
			return '_' + c.charCodeAt( 0 ) + '_';
		} );
	}

	function makeRequest( symbol ) {
		var request,
			data = querystring.stringify( {
				q: symbol.tex
			} ),
			// API call to mathoid
			options = {
				host: 'mathoid.testme.wmflabs.org',
				port: '80',
				path: '/',
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
					'Content-Length': Buffer.byteLength( data )
				}
			};
		// Populate and make the API call
		request = http.request( options, function ( res ) {
			var body = '';
			res.setEncoding( 'utf8' );

			res.on( 'data', function ( data ) {
				body += data;
			} );

			res.on( 'end', function () {
				var cssRule, buttonHeight, height, verticalAlign, heightDifference, offset,
					className = texToClass( symbol.tex ),
					data = JSON.parse( body ),
					svg = data.svg;

				if ( !svg ) {
					console.log( symbol.tex + ' FAILED: ' + body );
					onEnd();
					return;
				}

				cssRule = cssPrefix + className + ' {\n' +
					'\tbackground-image: url(data:image/svg+xml,' + encodeURIComponentForCSS( svg ) + ');\n';

				if ( symbol.alignBaseline ) {
					// Convert buttonHeight from em to ex, because SVG height is given in ex. (This is an
					// approximation, since the em:ex ratio differs from font to font.)
					buttonHeight = symbol.largeLayout ? singleButtonHeight * 4 : singleButtonHeight * 1.9931;
					// height and verticalAlign rely on the format of the SVG parameters
					// HACK: Adjust these by a factor of 0.8 to match VE's default font size of 0.8em
					height = parseFloat( data.mathoidStyle.match( /height\:\s*(.*)ex/ )[ 1 ] ) * 0.8;
					verticalAlign = -parseFloat( data.mathoidStyle.match( /vertical-align\:\s*(.*)ex/ )[ 1 ] ) * 0.8;
					// CSS percentage positioning is based on the difference between the image and container sizes
					heightDifference = buttonHeight - height;
					offset = 100 * ( verticalAlign - height + ( baseline * buttonHeight ) ) / heightDifference;

					cssRule += '\tbackground-position: 50% ' + offset + '%;\n' +
						'}';
					cssRules.push( cssRule );
					console.log( symbol.tex + ' -> ' + className );
				} else {
					cssRule += '}';
					cssRules.push( cssRule );
					console.log( symbol.tex + ' -> ' + className );
				}
				onEnd();

			} );
		} );
		request.setTimeout( 10000 );
		request.write( data );
		request.end();
		runNext();
	}

	function onEnd() {
		count--;
		runNext();
	}

	function runNext() {
		if ( count < mathoidMaxConnections && symbolList.length ) {
			count++;
			makeRequest( symbolList.shift() );
		}
		if ( !symbolList.length && !count ) {
			cssRules.sort();
			fs.writeFileSync(
				cssFile,
				'/*!\n' +
				' * This file is GENERATED by tools/makeSvgsAndCss.js\n' +
				' * DO NOT EDIT\n' +
				' */\n' +
				'\n' +
				cssRules.join( '\n\n' ) +
				'\n'
			);
		}
	}

	if ( cssData ) {
		cssLines = cssData.split( '\n' );
		for ( i = 0; i < cssLines.length; i++ ) {
			if ( cssLines[ i ].indexOf( cssPrefix ) === 0 ) {
				currentClassName = cssLines[ i ].slice( cssPrefix.length, -2 );
				currentRule.push( cssLines[ i ] );
				cssClasses[ currentClassName ] = false; // Default to false
			} else if ( currentRule.length ) {
				currentRule.push( cssLines[ i ] );
				if ( cssLines[ i ].indexOf( '\tbackground-position' ) === 0 ) {
					cssClasses[ currentClassName ] = true;
				}
				if ( cssLines[ i ].indexOf( '}' ) === 0 ) {
					cssRules.push( currentRule.join( '\n' ) );
					currentRule.splice( 0, currentRule.length );
				}
			}
		}
	}

	symbolObject = JSON.parse( symbolsData );
	for ( group in symbolObject ) {
		symbols = symbolObject[ group ];
		for ( i = 0; i < symbols.length; i++ ) {
			symbol = symbols[ i ];
			if ( symbol.duplicate || symbol.notWorking ) {
				continue;
			}
			currentClassName = texToClass( symbol.tex );
			alignBaseline = !symbol.alignBaseline;
			// If symbol is not in the old CSS file, or its alignBaseline status has changed,
			// add it to symbolList. Check to make sure it hasn't already been added.
			if ( cssClasses[ currentClassName ] === undefined ||
				( unmodifiedClasses[ currentClassName ] !== true &&
					cssClasses[ currentClassName ] === alignBaseline ) ) {
				symbolList.push( symbol );
			} else {
				// At the end of this loop, any CSS class names that aren't in unmodifiedClasses
				// will be deleted from cssRules. cssRules will then only contain rules that will
				// stay unmodified.
				unmodifiedClasses[ currentClassName ] = true;
			}
		}
	}

	// Keep only classes that will stay the same. Remove classes that are being adjusted and
	// classes of symbols that have been deleted from the JSON.
	cssRules = cssRules.filter( function ( rule ) {
		currentClassName = rule.split( '\n' )[ 0 ].slice( cssPrefix.length, -2 );
		if ( unmodifiedClasses[ currentClassName ] ) {
			return true;
		}
		console.log( 'Removing or adjusting: ' + currentClassName );
		return false;
	} );

	count = 0;
	runNext();

} )();
