'use strict';

/** @module lib/mscript */


interface RGB extends Array<number>{
	[index : number] : number;
}

const BLACK : string = '0,0,0';
const WHITE : string = '255,255,255';
const CMD : string[] = [
	'CF',
	'PF',
	'BF',
	'CB',
	'PB',
	'BB',
	'C2F',
	'C2B',
	'CCF',
	'CCB',
	'P2F',
	'P2B',
	'PPF',
	'PPB',
	'CFCB',
    'CBCF',
    'PFPB',
    'PBPF'
];

const CAMERA_SECONDARY : string[] = [
	'C2F',
	'C2B',
	'CCF',
	'CCB',
	'CFCB',
    'CBCF'
];

const PROJECTOR_SECONDARY : string[] = [
	'P2F',
	'P2B',
	'PPF',
	'PPB',
    'PFPB',
    'PBPF'
];

const ALTS : any = {
	'CF' : ['CAMERA FORWARD', 'CAM FORWARD'],
	'PF' : ['PROJECTOR FORWARD', 'PROJ FORWARD'],
	'BF' : ['BLACK FORWARD', 'BLACK', 'BLANK FORWARD', 'BLANK'],
	'CB' : ['CAMERA BACKWARD', 'CAM BACKWARD', 'CAMERA BACK', 'CAM BACK'],
	'PB' : ['PROJECTOR FORWARD', 'PROJ FORWARD', 'PROJECTOR BACK', 'PROJ BACK'],
	'BB' : ['BLACK BACKWARD', 'BLACK BACK', 'BLANK BACK'],
	'L ' : ['LIGHT', 'COLOR', 'LAMP'],
	'F ' : ['FADE'],
	'C2F' : ['CAMERA2 FORWARD', 'CAM2 FORWARD'],
    'C2B' : ['CAMERA2 BACKWARD', 'CAM2 BACKWARD', 'CAMERA2 BACK', 'CAM2 BACK'],
    'CCF' : ['CAMERAS FORWARD', 'CAMS FORWARD'],
    'CCB' : ['CAMERAS BACKWARD', 'CAMS BACKWARD', 'CAMERAS BACK', 'CAMS BACK'],
    'P2F' : ['PROJECTOR2 FORWARD', 'PROJ2 FORWARD'],
    'P2B' : ['PROJECTOR2 BACKWARD', 'PROJ2 BACKWARD', 'PROJECTOR2 BACK', 'PROJ2 BACK'],
    'PPF' : ['PROJECTORS FORWARD', 'PROJS FORWARD'],
    'PPB' : ['PROJECTORS BACKWARD', 'PROJS BACKWARD', 'PROJECTORS BACK', 'PROJS BACK'],

    'CFCB' : [ ],
    'CBCF' : [ ],
    'PFPB' : [ ],
    'PBPF' : [ ]
};

const DELAY : string = 'DELAY';
const PAUSE : string = 'PAUSE';
const ALERT : string = 'ALERT';

/** helper functions */

/** startswith function from lodash, do not want the entire lib for this 
 * @param str 		{string} Text to evaluate
 * @param target 	{string} Text to compare string against
 * @param position  {integer} Position in the string to make comparison at
 *
 * @returns {boolean} True for match, false for no match
 **/
function startsWith (str : string, target : string, position? : number) : boolean {
	const { length } = str;

	position = position == null ? 0 : position;

	if (position < 0) {
		position = 0;
	} else if (position > length) {
		position = length;
	}

	target = `${target}`;

	return str.slice(position, position + target.length) == target;
}

/** class Mscript */
export default class Mscript {
	output : any;
	lines : string[];
	cam : number;
	cam2 : number;
	proj : number;
	proj2 : number;
	color : string;
	loops : any[];
	rec : number;
	two : string;
	three : string;
	four : string;
	arr : any[];
	meta : string[];
	target : number;
	dist : number;
	variables : any;

	/**
	 * @constructor
	 * Create new Mscript interpreter
	 **/

	constructor () {
		this.output = {};
	}
	/**
	 * Clear the state of the script
	 */
	clear () {
		this.lines = [];

		this.cam = 0;
		this.cam2 = 0;
		this.proj = 0;
		this.proj2 = 0;
		this.color = '';
		this.loops = [];
		this.rec = -1;
		this.two = '';
		this.three = '';
		this.four = '';
		this.arr = [];
		this.meta = [];
		this.target = 0; //move to target using CAM # or PROJ #
		this.dist = 0;

		this.variables = {};

		this.output = {};
	}
	/**
	 * Main function, accepts multi-line string, parses into lines
	 * and interprets the instructions from the text. Returns an array
	 * of steps to be fed into the mcopy sequence.
	 * 
	 * @param {string} text  Mscript text to interpret
	 * @param {function} callback Function to call when string is interpreted
	 *
	 * @returns {object} if callback is not provided
	 */
	interpret (text : string, callback : Function = null) {
		this.clear()

		if (typeof text === 'undefined') {
			return this.fail('No input');
		}

		//split string into lines, each containing a command
		this.lines = text.split('\n');

		this.lines = this.lines.map(line => {
			line = line.replace(/\t+/g, ''); //strip tabs
			line = line.trim(); //remove excess whitespace before and after command
			line = line.toUpperCase();
			return line;
		})

		for (let line of this.lines) {
			this.two = line.substring(0, 2);
			this.three = line.substring(0, 3);
			this.four = line.substring(0, 4);
			if (CMD.indexOf(this.four) !== -1) {
				this.basic_cmd(line, this.four);
			} else if (CMD.indexOf(this.three) !== -1) {
				this.basic_cmd(line, this.three);
			} else if (CMD.indexOf(this.two) !== -1) {
				this.basic_cmd(line, this.two);
			} else if (startsWith(line, DELAY)) {
				this.delay(line);
			} else if (startsWith(line, PAUSE)) {
				this.pause(line);
			} else if (startsWith(line, ALERT)) {
				this.alert(line);
			} else if (startsWith(line, '@') || line.indexOf('@') !== -1) {
				this.variable(line);
			} else if (startsWith(line, 'LOOP')) {
				this.new_loop(line);
			} else if (startsWith(line, 'L ')) {
				this.light_state(line);
			} else if (startsWith(line, 'F ')) {
				this.new_loop(line, true);
			} else if (startsWith(line, 'END')) {
				this.end_loop(line);
			} else if (startsWith(line, 'CAM2')) { //directly go to that frame 
				this.move_cam2(line);
			} else if (startsWith(line, 'CAM')) { //directly go to that frame 
				this.move_cam(line);
			} else if (startsWith(line, 'PROJ2')) { //directly go to that frame
				this.move_proj2(line);
			} else if (startsWith(line, 'PROJ')) { //directly go to that frame
				this.move_proj(line);
			} else if (startsWith(line, 'SET')) { //set that state
				this.set_state(line);
			} else if (startsWith(line, '#') || startsWith(line, '//')) {
				//comments
				//ignore while parsing
			}
		}

		this.output.success = true;
		this.output.arr = this.arr; //all instructions
		this.output.meta = this.meta; //all metadata for instructions
		this.output.cam = this.cam;
		this.output.proj = this.proj;
		if (this.contains(this.arr, CAMERA_SECONDARY)) {
			this.output.cam2 = this.cam2;
		}
		if (this.contains(this.arr, PROJECTOR_SECONDARY)) {
			this.output.proj2 = this.proj2;
		}

		if (typeof callback !== 'undefined' && callback != null) {
			//should only be invoked by running mscript.tests()
			callback(this.output);
		} else {
			return this.output;
		}
	}
	/**
	 * Interprets variables for complex sequence behavior.
	 * TODO: Fully implement, add test coverage
	 *
	 * @param {string} line Line containing a variable assignment
	 *
	 **/
	variable (line : string) {
		let parts : string[] = line.split('=');
		let key : string = parts[0];
		let value : any = parts[1];
		let update : boolean = false;

		if (value && value.indexOf('#') !== -1) {
			value = value.split('#')[0];
		}

		if (value && value.indexOf('//') !== -1) {
			value = value.split('//')[0];
		}

		if (value && value.indexOf('+') !== -1) {
			if (value)
			update = true;
		}

		if (line.indexOf('-') !== -1) {
			update = true;
		}

		if (line.indexOf(',') === -1) { //if not color string
			try {
				value = parseInt(value);
			} catch (err) {
				//supress parsing error
			}
		}
		//console.dir(parts)
		if (!this.variables[key] || update) {
			this.variables[key] = value;
		}
		//console.dir(this.variables)
	}

	/**
	 * Replace variable with value at time of interpretation
	 * TODO: Implement this please
	 *
	 * @param {string} line Line containing variable to be replaced with value
	 *
	 * @returns {string} New string to be interpreted
	 **/
	variable_replace(line : string) : string {
		return line;
	}
	/**
	 * Interpret a basic two character command
	 * 
	 * @param {string} line Line of script to interpret
	 * @param {string} short The short command to use
	 */
	basic_cmd (line : string, short : string) {
		if (this.rec !== -1) {
			//hold generated arr in state loop array
			this.loops[this.rec].arr
				.push.apply(this.loops[this.rec].arr, 
							this.str_to_arr(line, 
							short));
			this.loops[this.rec].meta
				.push.apply(this.loops[this.rec].meta, 
							this.light_to_arr(line, 
							short));
		} else {
			this.arr.push.apply(this.arr, this.str_to_arr(line, short));
			this.meta.push.apply(this.meta, this.light_to_arr(line, short));
		}
	}
	/**
	 * Start a new loop
	 * 
	 * @param {string} line  	Line to evaluate as either loop or fade
	 * @param {boolean} fade 	Flag as true if fade
	 */
	new_loop (line : string, fade? : boolean) {
		this.rec++;
		this.loops[this.rec] = {
			arr : [],
			meta : [],
			cam : 0,
			proj : 0,
			cam2 : 0,
			proj2 : 0,
			cmd : line + ''
		};
		if (fade) {
			this.fade(line);
		}
	}
	/**
	 * Close the most recent loop
	 * 
	 * @param {string} line Line to interpret
	 */
	end_loop (line : string) {
		let meta_arr : string[];
		let start : RGB;
		let end : RGB;
		let len : number;
		
		for (let x = 0; x < this.loop_count(this.loops[this.rec].cmd); x++) {
			meta_arr = this.loops[this.rec].meta;
			if (this.loops[this.rec].fade) {
				start = this.loops[this.rec].start;
				end = this.loops[this.rec].end;
				len = this.loops[this.rec].fade_len;
				meta_arr = meta_arr.map(l => {
					return this.fade_rgb(start, end, len, x);
				})
			}
			if (this.rec === 0) {
				this.arr.push.apply(this.arr, this.loops[this.rec].arr);
				this.meta.push.apply(this.meta, meta_arr);
			} else if (this.rec >= 1) {
				this.loops[this.rec - 1].arr
					.push.apply(this.loops[this.rec - 1].arr, 
								this.loops[this.rec].arr);

				this.loops[this.rec - 1].meta
					.push.apply(this.loops[this.rec - 1].meta, 
								meta_arr);
			}
		}
		this.update('END', this.loop_count(this.loops[this.rec].cmd));
		delete this.loops[this.rec];
		this.rec--;
	}
	/**
	 * Move camera to explicitly-defined frame
	 * 
	 * @param {string} line Line to interpret with camera move statement
	 */
	move_cam (line : string) {
		this.target = parseInt(line.split('CAM ')[1]);
		if (this.rec !== -1) {
			if (this.target > this.cam) {
				this.dist = this.target - this.cam;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('BF');
					this.loops[this.rec].meta.push(BLACK);
					this.update('BF');
				} 
			} else {
				this.dist = this.cam - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('BB');
					this.loops[this.rec].meta.push(BLACK);
					this.update('BB');
				}
			}
		} else {
			if (this.target > this.cam) {
				this.dist = this.target - this.cam;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('BF');
					this.meta.push(BLACK);
					this.update('BF');
				} 
			} else {
				this.dist = this.cam - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('BB');
					this.meta.push(BLACK);
					this.update('BB');
				}
			}
		}
	}

	/**
	 * Move secondary camera to explicitly-defined frame
	 * 
	 * @param {string} line Line to interpret with camera move statement
	 */
	move_cam2 (line : string) {
		this.target = parseInt(line.split('CAM2 ')[1]);
		if (this.rec !== -1) {
			if (this.target > this.cam2) {
				this.dist = this.target - this.cam2;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('C2F');
					this.loops[this.rec].meta.push(BLACK);
					this.update('C2F');
				} 
			} else {
				this.dist = this.cam2 - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('C2B');
					this.loops[this.rec].meta.push(BLACK);
					this.update('C2B');
				}
			}
		} else {
			if (this.target > this.cam2) {
				this.dist = this.target - this.cam2;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('C2F');
					this.meta.push(BLACK);
					this.update('C2F');
				} 
			} else {
				this.dist = this.cam2 - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('C2B');
					this.meta.push(BLACK);
					this.update('C2B');
				}
			}
		}
	}

	/**
	 * Move projector to explicitly-defined frame
	 * 
	 * @param {string} line Line containing `move` statement to interpret
	 */
	move_proj (line : string) {
		this.target = parseInt(line.split('PROJ ')[1]);
		if (this.rec !== -1) {
			if (this.target > this.proj) {
				this.dist = this.target - this.proj;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('PF');
					this.loops[this.rec].meta.push('');
					this.update('PF');
				} 
			} else {
				this.dist = this.proj - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('PB');
					this.loops[this.rec].meta.push('');
					this.update('PB');
				} 
			}
		} else {
			if (this.target > this.proj) {
				this.dist = this.target - this.proj;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('PF');
					this.meta.push('');
					this.update('PF');
				} 
			} else {
				this.dist = this.proj - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('PB');
					this.meta.push('');
					this.update('PB');
				} 
			}
		}
	}

	/**
	 * Move projector to explicitly-defined frame
	 * 
	 * @param {string} line Line containing `move` statement to interpret
	 */
	move_proj2 (line : string) {
		this.target = parseInt(line.split('PROJ2 ')[1]);
		if (this.rec !== -1) {
			if (this.target > this.proj2) {
				this.dist = this.target - this.proj2;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('P2F');
					this.loops[this.rec].meta.push('');
					this.update('P2F');
				} 
			} else {
				this.dist = this.proj2 - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.loops[this.rec].arr.push('P2B');
					this.loops[this.rec].meta.push('');
					this.update('P2B');
				} 
			}
		} else {
			if (this.target > this.proj2) {
				this.dist = this.target - this.proj2;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('P2F');
					this.meta.push('');
					this.update('P2F');
				} 
			} else {
				this.dist = this.proj2 - this.target;
				for (let x = 0; x < this.dist; x++) {
					this.arr.push('P2B');
					this.meta.push('');
					this.update('P2B');
				} 
			}
		}
	}
	/**
	 * Set the state of either the cam or projector
	 *
	 * @param line {string} String containing set statement
	 */
	set_state (line : string) {
		if (startsWith(line, 'SET CAM2')) {
			parseInt(line.split('SET CAM2')[1]);
		} else if (startsWith(line, 'SET PROJ2')) {
			this.cam2 = parseInt(line.split('SET PROJ2')[1]);
		} else if (startsWith(line, 'SET CAM')) {
			this.cam = parseInt(line.split('SET CAM')[1]);
		} else if (startsWith(line, 'SET PROJ')) {
			this.proj = parseInt(line.split('SET PROJ')[1]);
		}
	}
	/**
	 * Return the last loop
	 *
	 * @returns {object}
	 */
	last_loop () : any {
		return this.loops[this.loops.length - 1];
	}
	/**
	 * Return the second-last loop
	 * 
	 * @returns {object} Loop array
	 */
	parent_loop () : any {
		return this.loops[this.loops.length - 2];
	}
	/**
	 * Extract the loop count integer from a LOOP cmd
	 *
	 * @returns {integer} Loop count in string parsed into integer
	 */
	loop_count (str : string) : number {
		return parseInt(str.split(' ')[1]);
	}
	/**
	 * Execute a fade of frame length, from color to another color
	 * 
	 * @param {string} line Line containing a fade initiator
	 */
	fade (line : string) {
		let len = this.fade_count(line);
		let start = this.fade_start(line);
		let end = this.fade_end(line);

		this.loops[this.rec].start = start;
		this.loops[this.rec].end = end;
		this.loops[this.rec].fade = true;
		this.loops[this.rec].fade_count = 0;
		this.loops[this.rec].fade_len = len;
	}
	/**
	 * Extract the fade length integer from a FADE cmd
	 *
	 * @param {string} str Line containing the length of fade in frames
	 */
	fade_count (str : string) {
		return parseInt(str.split(' ')[1]);
	}
	/**
	 * Extract the start color from a string
	 *
	 * @param {string} str Line containing the start color value in a fade initiator
	 *
	 * @returns {array} Array containing RGB color values
	 */
	fade_start (str :  string) : RGB {
		let color : string = str.split(' ')[2];
		return this.rgb(color.trim())
	}
	/**
	 * Extract the end color from a string
	 *
	 * @param {string} str Line containing the end color value in a fade initiator
	 *
	 * @returns {array} Array containing RGB color values
	 */
	fade_end (str : string) : RGB {
		let color : string = str.split(' ')[3];
		return this.rgb(color.trim())
	}
    /** 
     * Determine the state of a fade at a particular frame in the sequence, x
	 *
	 * @param {array} start Color the fade starts at
	 * @param {array} end Color the fade finishes at
	 * @param {integer} len Total length of the fade in frames
	 * @param {integer} x Position of the fade to get color value of
	 *
	 * @returns {array} Array containing RGB color values
	 */
	fade_rgb (start : RGB, end : RGB, len : number, x : number) {
		let cur = [];
		let diff;
		for (let i : number = 0; i < 3; i++) {
			if (x === len - 1) {
				cur[i] = end[i];
			} else if (start[i] >= end[i]) {
				diff = start[i] - end[i];
				cur[i] = start[i] - Math.round((diff / (len - 1)) * x);
			} else {
				diff = end[i] - start[i];
				cur[i] = start[i] + Math.round((diff / (len - 1)) * x);
			}
		}
		return this.rgb_str(cur);

	}
	/**
	 * Parse string into array of RGB color values. 0-255 octet.
	 * 
	 * @param {string} str String containing only color values as `#,#,#`
	 **/
	rgb (str : string) : RGB {
		let rgb = str.split(',');
		return rgb.map( (char : string) => {
			return parseInt(char);
		})
	} 
	/**
	 *  Cast RGB color values as string
	 * 
	 * @param {array} arr Array to join into string
	 *
	 * @returns {string} String of RGB values
	 **/
	rgb_str (arr : RGB) : string {
		return arr.join(',');
	}
	/**
	 * Increase the state of a specific object, such as the camera/projector,
	 * by the value defined in val.
	 *
	 * @param {string} cmd String representing command to interpret and update state
	 */
	update (cmd : string, val : number = 1) {
		if (cmd === 'END') {
			//I don't understand this loop
			for (let i = 0; i < val; i++) {
				if (this.rec === 0) {
					this.cam += this.loops[this.rec].cam;
					this.proj += this.loops[this.rec].proj;
				} else if (this.rec >= 1) {
					this.loops[this.rec - 1].cam += this.loops[this.rec].cam;
					this.loops[this.rec - 1].proj += this.loops[this.rec].proj;
				}
			}
		} else if (cmd === 'CF') {
			if (this.rec === -1) {
				this.cam += val;
			} else {
				this.loops[this.rec].cam += val;
			}
		} else if (cmd === 'CB') {
			if (this.rec === -1) {
				this.cam -= val;
			} else {
				this.loops[this.rec].cam -= val;
			}
		} else if (cmd === 'PF') {
			if (this.rec === -1) {
				this.proj += val;
			} else {
				this.loops[this.rec].proj += val;
			}		
		} else if (cmd === 'PB') {
			if (this.rec === -1) {
				this.proj -= val;
			} else {
				this.loops[this.rec].proj -= val;
			}		
		} else if (cmd === 'BF') {
			if (this.rec === -1) {
				this.cam += val;
			} else {
				this.loops[this.rec].cam += val;
			}		
		} else if (cmd === 'BB') {
			if (this.rec === -1) {
				this.cam -= val;
			} else {
				this.loops[this.rec].cam -= val;
			}
		} else if (cmd === 'C2F') {
			if (this.rec === -1) {
				this.cam2 += val;
			} else {
				this.loops[this.rec].cam2 += val;
			}
		} else if (cmd === 'C2B') {
			if (this.rec === -1) {
				this.cam2 -= val;
			} else {
				this.loops[this.rec].cam2 -= val;
			}
		} else if (cmd === 'CCF') {
			if (this.rec === -1) {
				this.cam += val;
				this.cam2 += val;
			} else {
				this.loops[this.rec].cam2 += val;
				this.loops[this.rec].cam2 += val;
			}
		} else if (cmd === 'CCB') {
			if (this.rec === -1) {
				this.cam -= val;
				this.cam2 -= val;
			} else {
				this.loops[this.rec].cam2 -= val;
				this.loops[this.rec].cam2 -= val;
			}
		} else if (cmd === 'P2F') {
			if (this.rec === -1) {
				this.proj2 += val;
			} else {
				this.loops[this.rec].proj2 += val;
			}
		} else if (cmd === 'P2B') {
			if (this.rec === -1) {
				this.proj2 -= val;
			} else {
				this.loops[this.rec].proj2 -= val;
			}
		} else if (cmd === 'PPF') {
			if (this.rec === -1) {
				this.proj += val;
				this.proj2 += val;
			} else {
				this.loops[this.rec].proj += val
				this.loops[this.rec].proj2 += val;
			}
		} else if (cmd === 'PPB') {
			if (this.rec === -1) {
				this.proj -= val;
				this.proj2 -= val;
			} else {
				this.loops[this.rec].proj -= val
				this.loops[this.rec].proj2 -= val;
			}
		} else if (cmd === 'CFCB') {
			if (this.rec === -1) {
				this.cam += val;
				this.cam2 -= val;
			} else {
				this.loops[this.rec].cam += val;
				this.loops[this.rec].cam2 -= val;
			}
		} else if (cmd === 'CBCF') {
			if (this.rec === -1) {
				this.cam -= val;
				this.cam2 += val;
			} else {
				this.loops[this.rec].cam -= val;
				this.loops[this.rec].cam2 += val;
			}
		} else if (cmd === 'PFPB') {
			if (this.rec === -1) {
				this.proj += val;
				this.proj2 -= val;
			} else {
				this.loops[this.rec].proj += val;
				this.loops[this.rec].proj2 -= val;
			}
		} else if (cmd === 'PBPF') {
			if (this.rec === -1) {
				this.proj -= val;
				this.proj2 += val;
			} else {
				this.loops[this.rec].proj -= val;
				this.loops[this.rec].proj2 += val;
			}
		} else if (cmd === 'L ') {

		}
	}
	/**
	 * Split string on command, turn into array of commands
	 * as long as count variable. Default 1.
	 *
	 * @param {string} str String to split
	 * @param {string} cmd String representing command to split at
	 * 
	 * @returns {array} Array containing commands
	 */
	str_to_arr (str : string, cmd : string) : string[] {
		const cnt : string[] = str.split(cmd);
		let c  : number = parseInt(cnt[1]);
		let arr  : any[] = [];
		if (cnt[1] === '') {
			c = 1;
		} else {
			c = parseInt(cnt[1]);
		}
		arr = new Array(c).fill(cmd);
		this.update(cmd, c);
		return arr;
	}
	/**
	 * Split a string on a command to extract data for light array
	 *
	 * @param {string} str String containing light command
	 * @param {string} cmd String representing command
	 *
	 * @returns {array} An RGB array containing the color values
	 */
	light_to_arr (str : string, cmd : string) : RGB {
		const cnt : string[] = str.split(cmd);
		let c : number = parseInt(cnt[1]);
		let arr : any[] = [];
		if (cnt[1] === '') {
			c = 1;
		} else {
			c = parseInt(cnt[1]);
		}
		for (let i : number = 0; i < c; i++) {
			if (cmd === 'CF'
			 || cmd === 'CB') {
				arr.push(this.color);
			} else if (cmd === 'BF'
					|| cmd === 'BB') {
				arr.push(BLACK);
			} else {
				arr.push('');
			}
		}
		return arr;
	}
	/**
	 * Split a string to extract an rgb color value
	 *
	 * @param {string} Color string assign to color property
	 */
	light_state (str : string) {
		//add parsers for other color spaces
		const color : string = str.replace('L ', '').trim();
		this.color = color;
	}

	/**
	 * Interpret a delay command
	 * 
	 * @param {string} line String containing delay command
	 **/
	 delay (line : string) {
	 	let lenStr : string = line.split(' ')[1] || ''
	 	let len : number;
	 	lenStr = lenStr.trim();

	 	try {
	 		len = parseInt(lenStr, 10); //clean up string or fail
	 	} catch (err) {
	 		len = 0;
	 	}

	 	if (isNaN(len)) {
	 		len = 0;
	 	}

	 	lenStr = String(len);

	 	if (this.rec !== -1) {
			//hold generated arr in state loop array
			this.loops[this.rec].arr
				.push('DE');
			this.loops[this.rec].meta
				.push(lenStr);
		} else {
			this.arr.push('DE');
			this.meta.push(lenStr);
		}
	 }

	 /**
	 * Interpret an alert command
	 * 
	 * @param {string} line String containing alert message
	 **/
	 alert (line : string) {
	 	let msg : string = line.split(' ')[1] || ''
	 	msg = msg.trim();
	 	if (this.rec !== -1) {
			//hold generated arr in state loop array
			this.loops[this.rec].arr
				.push('AL');
			this.loops[this.rec].meta
				.push(line);
		} else {
			this.arr.push('AL');
			this.meta.push(line);
		}
	 }

	 /**
	 * Interpret an pause command
	 * 
	 * @param {string} line String containing alert message
	 **/
	 pause (line : string) {
	 	const msg : string = "Paused script. Click OK to continue."
	 	if (this.rec !== -1) {
			//hold generated arr in state loop array
			this.loops[this.rec].arr
				.push('AL');
			this.loops[this.rec].meta
				.push(msg);
		} else {
			this.arr.push('AL');
			this.meta.push(msg);
		}
	 }

	/**
	 * Throw an error with specific message
	 *
	 * @param {string} msg Error message to print
	 */
	fail (msg : string) {
		throw new Error(msg);
	}

	/**
	 * Determine if array contains matching elements of 
	 * another array
	 * 
	 * @param {Array} arr Original array to compare
	 * @param {Array} arr2 Array to compare elements from
	 * 
	 * @returns {boolean} Whether arr contains elements in arr2
	 **/
	contains (arr : string[], arr2 : string[]) {
		return arr.some(r => arr2.includes(r));
	}
}

module.exports = Mscript;


/*

CAM # - go to camera frame #
PROJ # - go to projector frame #

CAM2 # - go to camera 2 frame #
PROJ # - go to projector 2 frame #

SET CAM # - sets camera count to #
SET PROJ # - sets projector count to #

SET CAM2 # - sets camera 2 count to #
SET PROJ2 # - sets projector 2 count to #

LOOP # - begin loop, can nest recursively, # times
END LOOP - (or END) closes loop

L #RGB - sets light to rgb value

FADE 24 0,0,0 255,255,255
END FADE

CF - Camera forwards
PF - Projector forwards
BF - Black forwards
CB - Camera backwards
PB - Projector backwards
BB - Black backwards
C2F - Camera 2 forwards
C2B - Camera 2 backwards
CCF - Both cameras forwards
CCB - Both cameras backwards
P2F - Projector 2 forwards
P2B - Projector 2 backwards
PPF - Both projectors forwards
PPB - Both projectors backwards

ALERT {MESSAGE} - Stop the sequence and requiure user interaction to complete

*/