// @flow
import * as React from 'react';
import {
	Row,
	Col,
	Button,
	Form, FormGroup,
	Label, Input,
	Card, CardHeader, CardBody,
	TabContent, TabPane, Nav, NavItem, NavLink,
	Alert,
	UncontrolledDropdown, DropdownToggle, DropdownMenu,
	InputGroup, InputGroupAddon,
} from 'reactstrap';

import { connect } from 'react-redux';

import cn from 'classnames';

import { changeObjFieldName, generateNewKey, copyObj } from '@helpers';
import { algorithmValidator, BUILTIN_TYPES, ANALYZER_RESULT_TYPES, getValidAlgorithmObj as getValidObj } from '@helpers/beat';
import type { AlgorithmValidatorObject, BeatObject } from '@helpers/beat';

import * as Selectors from '@store/selectors.js';

import * as Actions from '@store/actions.js';
import CacheInput from '../CacheInput.jsx';
import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import TypedField from '../TypedField.jsx';
import TemplateButton from '../EntityTemplateGenerationButton.jsx';
import InfoTooltip from '../InfoTooltip.jsx';
import ParameterCreate from '../ParameterCreate.jsx';

type Props = {
	data: BeatObject,
	algorithms: BeatObject[],
	libraries: BeatObject[],
	dataformats: BeatObject[],
	saveFunc: (BeatObject) => any,
	updateFunc: (BeatObject) => any,
};

type State = {
	// which tab the user is viewing (endpoints, params, etc.)
	activeTab: string,
	// algorithm-specific validity checking (alerts for common errors & warnings)
	validity: AlgorithmValidatorObject,
	// parameters can restrict their values to individual choices
	// this string represents a choice that is being edited
	choiceCache: string,
};

// represents an input or output value
// inputs/outputs are just an object with a "type" field
type IOEntry = {|
	type: string,
|};

// represents a collection of inputs/outputs
type IOObject = {
	[string]: IOEntry,
};

// represents a group/endpoint, optionally with outputs
type Group = {
	name: string,
	inputs: IOObject,
	outputs?: IOObject,
};

export class AlgorithmEditor extends React.Component<Props, State> {
	constructor(props: Props) {
		//console.log(`Creating AlgDetail`);
		super(props);
	}

	state = {
		activeTab: '0',
		validity: algorithmValidator(this.props.data, this.props.algorithms),
		choiceCache: '',
	}

	// if the data changes behind the scenes, update the editor with these changes
	componentDidUpdate = (prevProps: Props) => {
		if(this.props !== prevProps){
			this.setState({
				validity: this.getValidity(this.props.data, this.props.algorithms),
			});
		}
	}

	// get the result types (analyzer result types are restricted to a subset of all available types)
	getResultDfs = (): string[] => ANALYZER_RESULT_TYPES.concat(this.props.dataformats.filter(d => d.name.startsWith('plot')).map(d => d.name))

	// returns a validity object based on the given algorithm cache and list of all algorithms
	getValidity = (cache: BeatObject = this.props.data, algs: BeatObject[] = this.props.algorithms): AlgorithmValidatorObject => {
		return algorithmValidator(cache, algs.filter(o => o !== this.props.data));
	}

	// much like getValidity but just returns a bool
	// doesnt worry about warnings, just errors
	isValid = (cache: BeatObject): boolean => {
		const isValid = Object.entries(this.getValidity(cache))
		.filter(([name, bool]) => name !== (this.isAnalyzer() ? 'endpoint0OutputExists' : 'result0Exists'))
		.reduce((bool, [name, b]): boolean => (!!b) && bool, true)
		;

		return isValid;
	}

	// change the tab
	tabTo = (tab: string) => { if(this.state.activeTab !== tab) this.setState({ activeTab: tab }); }

	// is the algorithm an analyzer?
	// theres no flag set, so instead check for the existence of a 'splittable' field which only normal algorithms have
	isAnalyzer = (): boolean => { return !Object.keys(this.props.data.contents).includes('splittable');  }

	// helper to change a value in the "contents" subobject of an algorithm
	// (this is where the vast majority of change happens)
	changeContentsVal = (field: string, val: any) => {
		const newCache = {
			...this.props.data,
			contents: {
				...this.props.data.contents,
				[field]: val
			}
		};

		this.props.updateFunc(newCache);
	}

	// updates an old group object to a new group object
	updateGroup = (oldGroup: Group, newGroup: Group) => {
		const gIdx = this.props.data.contents.groups.findIndex(g => JSON.stringify(g) === JSON.stringify(oldGroup));
		const newGroups = copyObj(this.props.data.contents.groups);
		newGroups[gIdx] = newGroup;
		this.changeContentsVal('groups', newGroups);
	}

	// 4 different functions:
	// creates a parameter (provide name & value)
	// updates a parameter (provide name & value)
	// renames a parameter (provide name & value & the old name)
	// deletes a parameter (provide null as name, value is optional, the name)
	updateParameter = (name: null | string, value: any, oldName: ?string) => {
		const params = copyObj(this.props.data.contents.parameters);
		if(oldName)
			delete params[oldName];
		if(name !== null)
			params[name] = value;
		this.changeContentsVal('parameters', params);
	}

	// updates a result given the result name and the value obj
	updateResult = (name: string, res: any) => {
		const rs = this.props.data.contents.results;
		rs[name] = res;
		this.changeContentsVal('results', rs);
	}

	// toggles the algorithm to an analyzer algorithm
	changeToAnalyzer = () => {
		const contents = copyObj(this.props.data.contents);
		contents.groups = contents.groups.map(g => {
			delete g['outputs'];
			return g;
		});
		contents.results = {};
		delete contents['splittable'];

		const newCache = {
			...this.props.data,
			contents
		};

		this.props.updateFunc(newCache);
	}

	// toggles the algorithm to a normal algorithm
	changeToNormal = () => {
		const contents = copyObj(this.props.data.contents);
		delete contents.results;
		if(contents.groups.length > 0)
			contents.groups[0].outputs = {};
		contents.splittable = false;

		const newCache = {
			...this.props.data,
			contents
		};

		this.props.updateFunc(newCache);
	}

	// generates a func from a given group & type that takes an input/output name
	// and deletes it from the group
	deleteIO = (group: Group, type: 'inputs' | 'outputs') => (name: string) => {
		if(!group[type])
			return;
		delete group[type][name];
		this.updateGroup(group, { ...group });
	}

	// rendergs the endpoints tab
	renderEndpoints = () => (
		<TabPane tabId='0'>
			{
				// loop through all groups in the alg
				this.props.data.contents.groups.map((group: Group, i, groups: Group[]) => {
					// func to update an input/output
					// lets you change the name and/or the type
					const ioUpdate = (subObj: 'inputs' | 'outputs') => (oldName: string, newName: string, newObj: IOEntry) => {
						const changedObj = changeObjFieldName(group[subObj], oldName, newName);
						changedObj[newName] = newObj;
						this.updateGroup(group,
							{
								...group,
								[subObj]: changedObj
							}
						);
					};

					const inputsUpdate = ioUpdate('inputs');
					const outputsUpdate = ioUpdate('outputs');

					return (
						<Row key={i} className='mb-2' id={`endpoint${ i }`}>
							<Col sm='12'>
								<Card>
									{ groups.length > 1 &&
											<CardHeader>
												Group { i }
											</CardHeader>
									}
									<CardBody>
										{ Object.keys(group.inputs).length === 0 && i !== 0 &&
												<Alert color='warning'>
													In general, an endpoint group should have at least 1 input.
												</Alert>
										}
										<Row>
											<Col sm={group.outputs ? '6' : '12'}>
												{
													(Object.entries(group.inputs): [string, any][])
													.map(([name, ioObj], i, gEntries) =>
														<TypedField
															className='algInput'
															key={i}
															name={name}
															type={ioObj.type}
															types={this.props.dataformats.map(d => d.name)}
															existingFields={gEntries.map(([n, v]) => n)}
															nameUpdateFunc={str => inputsUpdate(name, str, ioObj)}
															typeUpdateFunc={str => inputsUpdate(
																name,
																name,
																{ type: str }
															)}
															deleteFunc={() => this.deleteIO(group, 'inputs')(name)}
														/>
													)
												}
												<Button outline block
													id='newInputBtn'
													onClick={(e) => {
														const newKey = generateNewKey('input', Object.keys(group.inputs));
														this.updateGroup(
															group,
															{
																...group,
																inputs: {
																	...group.inputs,
																	[newKey]: { type: '' }
																}
															}
														);
													}}
												>
													New Input
												</Button>
											</Col>
											{ group.outputs && (
												<Col sm='6'>
													{
														(Object.entries(group.outputs): [string, any][])
														.map(([name, ioObj], i, gEntries) =>
															<TypedField
																className='algOutput'
																key={i}
																name={name}
																type={ioObj.type}
																types={this.props.dataformats.map(d => d.name)}
																existingFields={gEntries.map(([n, v]) => n)}
																nameUpdateFunc={str => outputsUpdate(name, str, ioObj)}
																typeUpdateFunc={str => outputsUpdate(
																	name,
																	name,
																	{ type: str }
																)}
																deleteFunc={() => this.deleteIO(group, 'outputs')(name)}
															/>
														)
													}
													<Button outline block
														id='newOutputBtn'
														onClick={(e) => {
															const newKey = generateNewKey('output', Object.keys(group.outputs));
															this.updateGroup(
																group,
																{
																	...group,
																	outputs: {
																		...group.outputs,
																		[newKey]: { type: '' }
																	}
																}
															);
														}}
													>
														New Output
													</Button>
												</Col>
											)}
										</Row>
									</CardBody>
									{ i !== 0 &&
										<Button block color='danger' outline
											onClick={() => this.changeContentsVal(
												'groups',
												groups
												.filter(g => g.name !== group.name)
											)}
										>
											Delete Group
										</Button>
									}
								</Card>
							</Col>
						</Row>
					);
				})
			}
			<Button block
				id='newGroupBtn'
				onClick={(e) => {
					const newGroupName = generateNewKey('group', this.props.data.contents.groups.map(g => g.name));
					const newGroup = {
						name: newGroupName,
						inputs: {},
					};
					if(!this.isAnalyzer() && this.props.data.contents.groups.length === 0)
						newGroup.outputs = {};

					this.changeContentsVal(
						'groups',
						[... this.props.data.contents.groups, newGroup ]
					);
				}}
			>
				New Group
			</Button>
		</TabPane>
	);

	// renders the parameters tab
	renderParameters = () => {
		return (
			<TabPane tabId='1'>
				{
					// loop through all the parameters
					(Object.entries(this.props.data.contents.parameters): [string, any][]).map(([name, param], i, params) => (
						<Row key={i} className='mb-2'>
							<Col sm='12'>
								<ParameterCreate
									name={name}
									param={param}
									params={params}
									updateParameter={this.updateParameter}
								/>
							</Col>
						</Row>
					))
				}
				<Button outline block
					id='newParameterBtn'
					onClick={() => {
						const newKey = generateNewKey('parameter', Object.keys(this.props.data.contents.parameters || {}));
						this.changeContentsVal('parameters',
							{
								...(this.props.data.contents.parameters || {}),
								[newKey]: {
									type: '',
									default: '',
									description: '',
								}
							}
						);
					}}
				>
					New Parameter
				</Button>
			</TabPane>
		);
	}

	// renders the libraries tab
	renderLibraries = () => (
		<TabPane tabId='2'>
			<Row>
				<Col sm='12'>
					{
						// loop through all the used libs
						(Object.entries(this.props.data.contents.uses): [string, any][])
						.map(([name, lib], i, lEntries) => (
							<TypedField
								key={i}
								name={name}
								type={lib}
								types={this.props.libraries.map(l => l.name)}
								existingFields={lEntries.map(([n, v]) => n)}
								nameUpdateFunc={str => {
									// update the alias
									this.changeContentsVal('uses',
										changeObjFieldName(
											this.props.data.contents.uses,
											name,
											str
										)
									);
								}}
								typeUpdateFunc={str => {
									// update the chosen library for the alias
									const libs = {
										...this.props.data.contents.uses,
										[name]: str
									};
									this.changeContentsVal('uses', libs);
								}}
								deleteFunc={() => {
									const libs = { ...this.props.data.contents.uses };
									delete libs[name];
									this.changeContentsVal('uses', libs);
								}}
							/>
						))
					}
				</Col>
			</Row>
			<Button outline block
				id='newLibraryBtn'
				onClick={() => {
					const newKey = generateNewKey('library', Object.keys(this.props.data.contents.uses));
					this.changeContentsVal('uses',
						{
							...this.props.data.contents.uses,
							[newKey]: ''
						}
					);
				}}
			>
				New Library
			</Button>
		</TabPane>
	);

	// renders the results tab (only if the alg is an analyzer)
	renderResults = () => (
		<TabPane tabId='3'>
			{
				// loop through results
				(Object.entries(this.props.data.contents.results): [string, any][])
				.map(([name, result], i, rEntries) => (
					<TypedField
						key={i}
						name={name}
						type={result.type}
						types={this.getResultDfs()}
						existingFields={rEntries.map(([n, v]) => n)}
						nameUpdateFunc={str => this.changeContentsVal(
							'results',
							changeObjFieldName(
								this.props.data.contents.results,
								name,
								str
							)
						)}
						typeUpdateFunc={str => this.updateResult(name,
							{
								...result,
								type: str
							}
						)}
						deleteFunc={() => {
							const newRes = {
								...this.props.data.contents.results
							};
							delete newRes[name];
							this.changeContentsVal(
								'results',
								newRes
							);
						}}
					>
						<Col>
							{/* display by default checkbox */}
							<Label check>
								<Input
									type='checkbox'
									value={result.display ? true : false}
									onChange={(e) => this.changeContentsVal(
										'results',
										{ ...this.props.data.contents.results,
											[name] : {
												...result,
												display: e.target.checked
											}
										})
									}
								/>
								{' '}
								<span>Display By Default</span>
							</Label>
						</Col>
					</TypedField>
				))
			}
			<Button outline block
				id='newResultBtn'
				onClick={() => {
					const newKey = generateNewKey('result', Object.keys(this.props.data.contents.results));
					this.changeContentsVal(
						'results',
						{...this.props.data.contents.results,
							[newKey]: {
								display: false,
								type: ''
							}
						}
					);
				}}
			>
				New Result
			</Button>
		</TabPane>
	);

	render = () => (
		<div>
			{/* header content (save button & template gen button) */}
			<div className='d-flex'>
				<Button
					className='mx-auto'
					outline
					color='secondary'
					onClick={() => this.props.saveFunc(this.props.data)}
				>
					Save Changes (Changes are <ValidSchemaBadge entity='algorithm' obj={this.props.data} />)
				</Button>
				<TemplateButton
					data={this.props.data}
					entity={'algorithm'}
				/>
			</div>
			{/* simple universal stuff - description, what type of alg, if its splittable */}
			<Form>
				<FormGroup tag='fieldset'>
					<FormGroup>
						<Label for='algDesc'>Short Description</Label>
						<Input
							type='text'
							name='desc'
							id='algDesc'
							placeholder='Algorithm description...'
							value={this.props.data.contents.description}
							onChange={(e) => this.changeContentsVal('description', e.target.value)}
						/>
					</FormGroup>
					<FormGroup check>
						<Label check>
							<Input
								id='algAnalyzer'
								type='checkbox'
								checked={this.isAnalyzer()}
								onChange={e => e.target.checked ? this.changeToAnalyzer() : this.changeToNormal()}
							/>{' '}
							<InfoTooltip
								id='analyzerTooltip'
								info={`Checking this option makes this algorithm an "analyzer". Analyzers are made to analyze the experiment's performance (and other characteristics). Experiments must have 1 and only analyzer at the end of the experiment's toolchain. These types of algorithms produce evalution data for the user, such as plots, scores, and statistics.`}
							>
								Analyser
							</InfoTooltip>
						</Label>
					</FormGroup>
					{ !this.isAnalyzer() &&
							<FormGroup check>
								<Label check>
									<Input
										id='algSplittable'
										type='checkbox'
										checked={this.props.data.contents.splittable ? true : false}
										onChange={e => this.changeContentsVal('splittable', e.target.checked)}
									/>{' '}
									<InfoTooltip
										id='splittableTooltip'
										info={`A non-analyzer algorithm may be decided to be "splittable". Being splittable means that multiple instances of the algorithm may be ran in parallel on different bits of input. Non-splittable algorithms usually have some sort of aggregation/cross-input activity. Most algorithms will be splittable, but it is much easier to debug an algorithm with it being non-splittable.`}
									>
										Splittable
									</InfoTooltip>
								</Label>
							</FormGroup>
					}
				</FormGroup>
				{/* most errors/warnings from the current AlgorithmValidator object are displayed here */}
				{
					(!this.isAnalyzer() || this.getValidity().result0Exists) ||
						<Alert color='danger'>
							You need at least 1 result from an Analyzer.
						</Alert>
				}
				{
					this.getValidity().endpoint0Exists ||
						<Alert color='danger'>
							You need at least 1 endpoint group.
						</Alert>
				}
				{
					(this.isAnalyzer() || this.getValidity().endpoint0OutputExists) ||
						<Alert color='danger'>
							You must have at least 1 output
							in the first endpoint group.
						</Alert>
				}
				{
					this.getValidity().endpoint0InputExists ||
						<Alert color='danger'>
							You need at least 1 input in the first endpoint group.
						</Alert>
				}
				{/* all the tabs: endpoints, params, libs, results */}
				<Nav tabs className='mt-3 mb-3'>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '0' })}
							onClick={() => this.tabTo('0')}
						>
							<InfoTooltip
								id='endpointTooltip'
								info={`Endpoints are groups of inputs & outputs. An algorithm can have multiple endpoints (and must have at least 1), but each endpoint must have at least 1 input. If the algorithm is an analyzer, no groups can have outputs. If it is not an analyzer, only the first endpoint can have outputs, and at least 1 output is required.`}
							>
								Endpoints
							</InfoTooltip>
						</NavLink>
					</NavItem>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '1' })}
							onClick={() => this.tabTo('1')}
						>
							<InfoTooltip
								id='parametersTooltip'
								info={`Parameters are completely optional, allowing for users to change the functionality of the block when configuring an experiment. Parameters lend themselves to building reusable and flexible blocks. For example, a parameter could allow configuring the number of components in PCA, or choosing kernels/kernel parameters for an SVM.`}
							>
								Parameters
							</InfoTooltip>
						</NavLink>
					</NavItem>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '2' })}
							onClick={() => this.tabTo('2')}
						>
							<InfoTooltip
								id='libraryTooltip'
								info={`Libraries provide DRYness to code development for objects in the BEAT platform. Libraries are groups of functions under a given namespace. You may use libraries by adding it to your algorithm and referencing the namespace in your algorithm by the nickname you provide here.`}
							>
								Libraries
							</InfoTooltip>
						</NavLink>
					</NavItem>
					{ this.isAnalyzer() &&
							<NavItem>
								<NavLink
									className={cn({ active: this.state.activeTab === '3' })}
									onClick={() => this.tabTo('3')}
								>
									<InfoTooltip
										id='resultsTooltip'
										info={`Results are only for analyzer algorithms. They are similar to outputs in normal algorithms, but provide evaluation information on the whole experiment instead of output data to be processed by another block. Examples are plot data (for example, data for an ROC curve), scores, and statistics.`}
									>
										Results
									</InfoTooltip>
								</NavLink>
							</NavItem>
					}
				</Nav>
				<TabContent className='mb-3' activeTab={this.state.activeTab}>
					{ this.renderEndpoints() }
					{ this.renderParameters() }
					{ this.renderLibraries() }
					{ this.props.data.contents.results && this.renderResults() }
				</TabContent>
			</Form>
		</div>
	);
}

const mapStateToProps = (state, ownProps) => {
	const algs = Selectors.algorithmGet(state);
	const obj = {
		algorithms: algs,
		libraries: Selectors.libraryGet(state),
		dataformats: Selectors.dataformatGet(state),
		data: algs[ownProps.index] || getValidObj()
	};
	return obj;
};

const mapDispatchToProps = (dispatch, ownProps) => ({
	// replace the obj in the Redux store with the new object
	updateFunc: (obj) => {
		console.log(`dispatching for ${ obj.name }`);
		dispatch(Actions[`algorithmUpdate`](obj.name, obj));
	},
});

export default connect(mapStateToProps, mapDispatchToProps)(AlgorithmEditor);
