// @flow
// BEAT-specific helpers
import { copyObj } from '.';
import type { Contents as ToolchainContents, Group } from './toolchainTypes';
// all the BEAT entities
export type BeatEntity = 'database' | 'library' | 'dataformat' | 'algorithm' | 'toolchain' | 'experiment' | 'plotter' | 'plotterparameter';

// format of an instance of a beat entity in redux store
export type BeatObject = {|
	// name of the object
	name: string,
	// contents (whats stored in the JSON files in the beat prefix)
	contents: any,
	// extra contents that are additions, arent relevant to the web platform, and dont conform to the BEAT schemas
	extraContents?: {
		groups: Group[]
	},
|};

// format for the settings object
export type BeatSettings = {|
	prefix: string
|};

// format for an environment object (info about the available docker envs)
export type BeatEnvironment = {|
	name: string,
	packages: { [string]: string },
	queues: {
		[string]: {
			memory_limit: number,
			nb_slots: number,
			max_slots_per_user: number,
			nb_cores_per_slot: number,
			time_limit: number,
		},
	},
	accessibility: string,
	languages: [ string ],
	version: string,
	short_description: string,
|};

export const BEAT_ENTITIES: BeatEntity[] = [
	'database',
	'library',
	'dataformat',
	'algorithm',
	'toolchain',
	'experiment',
	'plotter',
	'plotterparameter',
];

// types auto-recognized by beat.core
export const BUILTIN_TYPES = [
	'int8',
	'int16',
	'int32',
	'int64',
	'uint8',
	'uint16',
	'uint32',
	'uint64',
	'float32',
	'float64',
	'bool',
	'string',
];

// extra types that are supported by
// some parameters/editors
export const DATAFORMAT_TYPES = [
	...BUILTIN_TYPES,
	'complex64',
	'complex128',
	'array',
	'object',
];

// result types for analyzers
export const ANALYZER_RESULT_TYPES = [
	'int32',
	'float32',
	'bool',
	'string'
];

Object.freeze(BEAT_ENTITIES);
Object.freeze(BUILTIN_TYPES);
Object.freeze(DATAFORMAT_TYPES);
Object.freeze(ANALYZER_RESULT_TYPES);

// get toolchain name from experiment name
export const getTcFromExpName = (expName: string) => expName.split('/').slice(1, 4).join('/');

// default object for beat entity instances
export const getDefaultEntityObject = () => ({name: '', contents: {}});

// most fields/name of/in beat objects need to conform to this regex
export const rxField = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;

// how many segments (separated by '/') are in an entity's name
export const nameSegmentsForEntity = (entity: BeatEntity) => {
	if(entity === 'database')
		return 2;
	if(entity === 'experiment')
		return 5;
	return 3;
};

// pluralizes an entity
export const pluralize = (be: BeatEntity): string => {
	switch(be) {
		case 'database':
			return 'databases';
		case 'library':
			return 'libraries';
		case 'dataformat':
			return 'dataformats';
		case 'algorithm':
			return 'algorithms';
		case 'toolchain':
			return 'toolchains';
		case 'experiment':
			return 'experiments';
		case 'plotter':
			return 'plotters';
		case 'plotterparameter':
			return 'plotterparameters';
		default:
			throw new Error(`Cannot pluralize invalid BE: "${ be }"`);
	}
};

// validates that a name for beat object of an entity is fine, given the names for all objects of that entity
export const nameValidator = (be: BeatEntity, allNames?: string[] = []) => (name: string): boolean => {
	if(allNames.includes(name)){
		return false;
	}

	const rxThreeSeg = /^[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/\d+$/;
	const rxTwoSeg = /^[A-Za-z0-9_]+\/\d+$/;
	const rxFiveSeg = /^[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/\d+\/[A-Za-z0-9_-]+$/;
	if(be !== 'database' && be !== 'experiment' && !rxThreeSeg.test(name))
		return false;
	if(be === 'database' && !rxTwoSeg.test(name))
		return false;
	if(be === 'experiment' && !rxFiveSeg.test(name))
		return false;

	const segs = name.split('/');
	if(segs.length < 2){
		return false;
	}
	if(segs[0] === '/') {
		return false;
	}
	if(segs[segs.length - 1] === '/'){
		return false;
	}

	return true;
};

// some editors are prime targets for better/additional validation

export type ValidatorObjectField = boolean | boolean[];

// only algs & libs have this extra validation right now
export type ValidatorObject = AlgorithmValidatorObject | LibraryValidatorObject;

export type ValidatorFunc = (obj: BeatObject, allObjs: BeatObject[]) => ValidatorObject;

// algorithm validator info
export type AlgorithmValidatorObject = {
	// is algorithm name valid?
	name: ValidatorObjectField,
	// is there at least one endpoint group?
	endpoint0Exists: ValidatorObjectField,
	// does the first endpoint have at least 1 input field?
	endpoint0InputExists: ValidatorObjectField,
	// does the first endpoint have at least 1 output field?
	endpoint0OutputExists: ValidatorObjectField,
	// does each endpoint have a unique name?
	endpointNamesUnique: ValidatorObjectField,
	// is there at least 1 result?
	result0Exists: ValidatorObjectField,
};

// lots of checks for algs
export const algorithmValidator = (obj: BeatObject, allObjs: BeatObject[]): AlgorithmValidatorObject => {

	let name = false;
	try {
		name = nameValidator('algorithm', allObjs.map(o => o.name))(obj.name);
	} catch(e){}

	let endpoint0Exists = false;
	try {
		endpoint0Exists = obj.contents.groups.length > 0;
	} catch(e){}

	let endpoint0InputExists = false;
	try {
		endpoint0InputExists = Object.keys(obj.contents.groups[0].inputs).length > 0;
	} catch(e){}

	let endpoint0OutputExists = false;
	try {
		endpoint0OutputExists = Object.keys(obj.contents.groups[0].outputs).length > 0;
	} catch(e){}

	let endpointNamesUnique = false;
	try {
		endpointNamesUnique = Array.from(new Set(obj.contents.groups.map(g => g.name))).length === obj.contents.groups.length;
	} catch(e){}

	let result0Exists = false;
	try {
		result0Exists = Object.keys(obj.contents.results).length > 0;
	} catch(e){}

	const valid: AlgorithmValidatorObject = {
		name,
		endpoint0Exists,
		endpoint0InputExists,
		endpoint0OutputExists,
		endpointNamesUnique,
		result0Exists,
	};

	return valid;
};

// extra validation for libs
export type LibraryValidatorObject = {
	// is library name valid?
	name: ValidatorObjectField,
	// is language valid?
	language: ValidatorObjectField,
};

export const libraryValidator = (obj: BeatObject, allObjs: BeatObject[]): LibraryValidatorObject => {
	let name = false;
	try {
		name = nameValidator('library', allObjs.map(o => o.name))(obj.name);
	} catch(e) {
	}

	let language = false;
	try {
		language = obj.contents.language === 'python';
	} catch(e) {
	}

	return {
		name,
		language,
	};
};

// these next several funcs, the "getValid***Obj", returns a properly-formed JSON obj,
// optionally filling in fields with an existing obj.
// basically they exist to keep malformed JSON from breaking the editors.
//
export const getValidAlgorithmObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			parameters: {},
			uses: {},
			groups: [
				{
					name: 'group',
					inputs: {'input': { type: '' }},
					outputs: {'output': { type: '' }},
				}
			],
			description: '',
			language: 'python',
			type: 'sequential',
			api_version: 2.0,
			schema_version: 3.0,
			...getObj.contents,
		},
	};

	if(!obj.contents.hasOwnProperty('results') && !obj.contents.hasOwnProperty('splittable'))
		obj.contents.splittable = false;

	return obj;
};

export const getValidDatabaseObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	if(Array.isArray(getObj.contents.protocols)){
		getObj.contents.protocols = getObj.contents.protocols
		.map(p => {
			p.sets = p.sets.map(s => ({parameters: {}, ...s}));
			return p;
		});
	}

	const obj = {
		...getObj,
		contents: {
			'description': '',
			'root_folder': '',
			'protocols': [],
			...getObj.contents,
		},
	};

	return obj;
};

export const getValidDataformatObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			'#description': '',
			...getObj.contents,
		},
	};

	return obj;
};

export type ParameterValue = number | string | boolean;

// returns the default value for a specific parameter, given the parameter object
export const expGetDefaultParameterValue = (parameter: any): ParameterValue => {
	const parsedDefault = Number.parseFloat(parameter.default);
	if(parameter.default)
		return Number.isNaN(parsedDefault) ? parameter.default : parsedDefault;
	if(parameter.range)
		return parameter.range[0];
	if(parameter.choice && parameter.choice.length > 0)
		return parameter.choice.length[0];

	switch(parameter.type){
		case 'string':
			return '';
		case 'bool':
			return false;
		// number
		default:
			return 0;
	}
};

export const getValidExperimentObj = (data: BeatObject = {name: '', contents: {}}, tc: BeatObject, algs: BeatObject[]) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	getObj.contents.datasets = getObj.contents.datasets || {};
	getObj.contents.blocks = getObj.contents.blocks || {};
	getObj.contents.analyzers = getObj.contents.analyzers || {};

	const datasets = tc.contents.datasets
	.map(ds => ({[ds.name]: getObj.contents.datasets[ds.name] || {set: '', protocol: '', database: ''}}))
	.reduce((o, ods) => ({...o, ...ods}), {...getObj.contents.datasets});

	const blocks = tc.contents.blocks
	.map(b => ({
		[b.name]: {
			algorithm: '',
			inputs: b.inputs ? b.inputs.reduce((o, inp) => ({...o, [inp]: ''}), {}) : {},
			outputs: b.outputs ? b.outputs.reduce((o, out) => ({...o, [out]: ''}), {}) : {},
			parameters: {},
			...(getObj.contents.blocks[b.name] || {})
		}
	}))
	.reduce((o, blk) => ({...o, ...blk}), {...getObj.contents.blocks});

	const analyzers = tc.contents.analyzers
	.map(a => ({
		[a.name]: {
			algorithm: '',
			inputs: a.inputs ? a.inputs.reduce((o, inp) => ({...o, [inp]: ''}), {}) : {},
			parameters: {},
			...(getObj.contents.analyzers[a.name] || {})
		}
	}))
	.reduce((o, ana) => ({...o, ...ana}), {...getObj.contents.analyzers});

	const paramAlgs = Object.entries({...blocks, ...analyzers})
	.map(([bName, block]) => algs.find(a => a.name === block.algorithm))
	.filter(a => a !== null && a !== undefined && a.contents.parameters !== undefined && Object.keys(a.contents.parameters).length > 0)
	.map(a => ({
		[a.name]: Object.entries(a.contents.parameters)
		.map(([pName, param]) => ({[pName]: expGetDefaultParameterValue(param)}))
		.reduce((o, p) => ({...o, ...p}), {})
	}))
	.reduce((o, p) => ({...o, ...p}), {});
	;

	const globals = {
		queue: 'Default',
		environment: {
			version: '0.0.4',
			name: 'Scientific Python 2.7'
		},
		...paramAlgs,
		...getObj.contents.globals
	};


	const obj = {
		...getObj,
		contents: {
			description: '',
			...getObj.contents,
			globals,
			datasets,
			blocks,
			analyzers,
		},
	};

	return obj;
};

export const getValidLibraryObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			'description': '',
			'language': 'python',
			'uses': {},
			...getObj.contents,
		},
	};

	return obj;
};

export const getValidToolchainObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		extraContents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			'description': '',
			datasets: [],
			blocks: [],
			analyzers: [],
			connections: [],
			representation: {
				connections: {},
				blocks: {},
				channel_colors: {},
			},
			...getObj.contents,
		},
		extraContents: {
			groups: [],
			...getObj.extraContents
		}
	};

	return obj;
};

export const getValidPlotterObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			description: '',
			language: 'python',
			parameters: {},
			dataformat: '',
			uses: {},
			...getObj.contents,
		}
	};

	return obj;
};

export const getValidPlotterparameterObj = (data: BeatObject = {name: '', contents: {}}) => {
	const getObj = {
		name: '',
		contents: {},
		...copyObj(data)
	};

	const obj = {
		...getObj,
		contents: {
			description: '',
			...getObj.contents,
		}
	};

	return obj;
};
