/**
 * LDraw object packer
 *
 * Usage:
 *
 * - Download official parts library from LDraw.org and unzip in a directory (e.g. ldraw/)
 * 	 - On Ubuntu: `sudo apt install ldraw-parts` installs to /usr/share/ldraw
 * 
 * - Update the ldrawPath variable below
 *
 * - Issue command 'node packLDrawModel <path to model (.ldr file)>'
 *
 * The packed object file will be output to the same folder as the input file. 
 * It will be named <modelFileName>_Packed.mpd and will contain all the object subtree as embedded files.
 *
 */

// var ldrawPath = './'; if you want to place this script in the ldraw folder
var ldrawPath = '/usr/share/ldraw';
var materialsFileName = 'LDConfig.ldr';


var fs = require( 'fs' );
var path = require( 'path' );

if ( process.argv.length !== 3 ) {

	console.log( "Usage: node packLDrawModel <modelFilePath>" );
	exit( 0 );

}
var fileName = process.argv[ 2 ];

var materialsFilePath = path.join( ldrawPath, materialsFileName );

console.log( 'Loading materials file "' + materialsFilePath + '"...' );
var materialsContent = fs.readFileSync( materialsFilePath, { encoding: "utf8" } );

console.log( 'Packing "' + fileName + '"...' );

var objectsPaths = [];
var objectsContents = [];
var pathMap = {};
var listOfNotFound = [];

// Parse object tree
parseObject( fileName, true );

// Check if previously files not found are found now
// (if so, probably they were already embedded)
var someNotFound = false;
for ( var i = 0; i < listOfNotFound.length; i ++ ) {

	if ( ! pathMap[ listOfNotFound[ i ] ] ) {

		someNotFound = true;
		console.log( 'Error: File object not found: "' + fileName + '".' );

	}

}

if ( someNotFound ) {

	console.log( "Some files were not found, aborting." );
	process.exit( - 1 );

}

// Obtain packed content
var packedContent = materialsContent + "\n";
for ( var i = objectsPaths.length - 1; i >= 0; i -- ) {

	packedContent += objectsContents[ i ];

}
packedContent += "\n";

// Save output file
var outPath = fileName + "_Packed.mpd";
console.log( 'Writing "' + outPath + '"...' );
fs.writeFileSync( outPath, packedContent );

console.log( 'Done.' );


//

function parseObject( fileName, isRoot ) {

	// Returns the located path for fileName or null if not found

	console.log( 'Adding "' + fileName + '".' );

	var originalFileName = fileName;

	var prefix = "";
	var objectContent = null;
	for ( var attempt = 0; attempt < 2; attempt ++ ) {

		prefix = "";

		if ( attempt === 1 ) {

			fileName = fileName.toLowerCase();

		}

		if ( fileName.startsWith( '48/' ) ) {

			prefix = "p/";

		} else if ( fileName.startsWith( 's/' ) ) {

			prefix = "parts/";

		}

		var absoluteObjectPath = path.resolve( fileName );

		try {

			objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
			break;

		} catch ( e ) {

			absoluteObjectPath = path.join( ldrawPath, fileName );

			try {

				objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
				break;
	
			} catch ( e ) {
	
				prefix = "parts/";
				absoluteObjectPath = path.join( ldrawPath, prefix, fileName );
	
				try {

					objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
					break;

				} catch ( e ) {

					prefix = "p/";
					absoluteObjectPath = path.join( ldrawPath, prefix, fileName );

					try {

						objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
						break;

					} catch ( e ) {

						try {

							prefix = "models/";
							absoluteObjectPath = path.join( ldrawPath, prefix, fileName );

							objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
							break;

						} catch ( e ) {
							if ( attempt === 1 ) {

								// The file has not been found, add to list of not found
								listOfNotFound.push( originalFileName );
								console.error(e);

							}

						}

					}

				}

			}

		}

	}

	var objectPath = path.join( prefix, fileName );

	if ( ! objectContent ) {

		// File was not found, but could be a referenced embedded file.
		return null;

	}

	if ( objectContent.indexOf( '\r\n' ) !== - 1 ) {

		// This is faster than String.split with regex that splits on both
		objectContent = objectContent.replace( /\r\n/g, '\n' );

	}

	var processedObjectContent = isRoot ? "" : "0 FILE " + objectPath + "\n";

	var lines = objectContent.split( "\n" );

	for ( var i = 0, n = lines.length; i < n; i ++ ) {

		var line = lines[ i ];
		var lineLength = line.length;

		// Skip spaces/tabs
		var charIndex = 0;
		while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) {

			charIndex ++;

		}
		line = line.substring( charIndex );
		lineLength = line.length;
		charIndex = 0;


		if ( line.startsWith( '0 FILE ' ) ) {

			if ( i === 0 ) {

				// Ignore first line FILE meta directive
				continue;

			}

			// Embedded object was found, add to path map

			var subobjectFileName = line.substring( charIndex ).trim().replace( /\\/g, '/' );

			if ( subobjectFileName ) {

				// Find name in path cache
				var subobjectPath = pathMap[ subobjectFileName ];

				if ( ! subobjectPath ) {

					pathMap[ subobjectFileName ] = subobjectFileName;

				}

			}

		}

		if ( line.startsWith( '1 ' ) ) {

			// Subobject, add it
			charIndex = 2;

			// Skip material, position and transform
			for ( var token = 0; token < 13 && charIndex < lineLength; token ++ ) {

				// Skip token
				while ( line.charAt( charIndex ) !== ' ' && line.charAt( charIndex ) !== '\t' && charIndex < lineLength ) {

					charIndex ++;

				}

				// Skip spaces/tabs
				while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) {

					charIndex ++;

				}

			}

			var subobjectFileName = line.substring( charIndex ).trim().replace( /\\/g, '/' );

			if ( subobjectFileName ) {

				// Find name in path cache
				var subobjectPath = pathMap[ subobjectFileName ];

				if ( ! subobjectPath ) {

					// Add new object
					subobjectPath = parseObject( subobjectFileName );

				}

				pathMap[ subobjectFileName ] = subobjectPath ? subobjectPath : subobjectFileName;

				processedObjectContent += line.substring( 0, charIndex ) + pathMap[ subobjectFileName ] + "\n";

			}

		} else {

			processedObjectContent += line + "\n";

		}

	}

	if ( objectsPaths.indexOf( objectPath ) < 0 ) {

		objectsPaths.push( objectPath );
		objectsContents.push( processedObjectContent );

	}

	return objectPath;

}
