// @flow
import * as React from 'react';
import {
	Container,
	Row,
	Col,
	Button,
	Form,
	FormGroup,
	Label,
	Input,
	InputGroup,
	FormText,
	Collapse,
	Card,
	CardHeader,
	CardBody,
	TabContent, TabPane, Nav, NavItem, NavLink, CardTitle, CardText,
	FormFeedback,
	Alert,
	InputGroupAddon,
	Dropdown, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
	ButtonGroup,
	ListGroup, ListGroupItem,
	UncontrolledTooltip,
} from 'reactstrap';

import { connect } from 'react-redux';

import './DatabaseEditor.css';

import { changeObjFieldName, copyObj } from '@helpers';
import { getValidDatabaseObj as getValidObj } from '@helpers/beat';
import type { BeatObject } from '@helpers/beat';
import fuse from 'fuse.js';

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

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


type Props = {
	data: BeatObject,
	saveFunc: (BeatObject) => any,
	databases: BeatObject[],
	dataformats: BeatObject[],
	updateFunc: (BeatObject) => any,
	protocols: SelectorProtocol[]
};

export type Protocol = {
	name: string,
	template: string,
	sets: Set[],
};

// the protocols from the databaseProtocols selector has an additional field
// that's not in the protocols in database objects
type SelectorProtocol = Protocol & { database: string };

type State = {
	activeProtocol: number,
	insertProtocolOpen: boolean,
	searchResults: SelectorProtocol[],
};

export type Set = {
	name: string,
	template: string,
	view: string,
	parameters: {
		[string]: string,
	},
	outputs: {
		[string]: string,
	},
};

const FuseOptions = {
	shouldSort: true,
	tokenize: true,
	threshold: 0.6,
	location: 0,
	distance: 100,
	maxPatternLength: 32,
	minMatchCharLength: 1,
	keys: ['template'],
};
let Fuse;

export class DatabaseEditor extends React.Component<Props, State> {
	constructor(props: Props) {
		super(props);
	}

	state = {
		activeProtocol: 0,
		insertProtocolOpen: false,
		searchResults: [],
	}

	setContents = (newContents: any) => {
		this.props.updateFunc({
			...this.props.data,
			contents: {
				'description': this.props.data.contents['description'],
				...newContents,
			}
		});
	}

	componentDidMount = () => {
		this.updateFuseInstance();
	}

	componentDidUpdate = (prevProps: Props, prevState: State) => {
		this.updateFuseInstance(prevProps);
	}

	updateFuseInstance = (prevProps?: Props) => {
		// update fuse & reset results if search data changed
		if(!prevProps || this.props.protocols !== prevProps.protocols){
			Fuse = new fuse(this.props.protocols, FuseOptions);
		}
	}

	activateProtocol = (index: number) => {
		this.setState({ activeProtocol: index });
	}

	updateProtocol = (index: number, obj: Protocol) => {
		this.setContents({
			...this.props.data.contents,
			protocols: this.props.data.contents.protocols.map((o, i) => i === index ? obj : o)
		});
	}

	updateSet = (idxProtocol: number, idxSet: number, obj: Set) => {
		const protocol = this.props.data.contents.protocols[idxProtocol];
		this.updateProtocol(
			idxProtocol,
			{
				...protocol,
				sets: protocol.sets.map((s, i) => i === idxSet ? obj : s)
			}
		);
	}

	toggleInsertProtocol = (val: boolean = !this.state.insertProtocolOpen) => {
		this.setState({ insertProtocolOpen: val });
	};

	protocolSearch = (str: string) => {
		const res = Fuse.search(str);
		this.setState(prevState => ({
			searchResults: res.slice(0, 5),
			insertProtocolOpen: res.length > 0,
		}));
	};

	// if the protocol template name has any uses with a different protocol makeup,
	// returns the info on all other uses
	validateProtocolTemplate = (protocol: Protocol): null | React.Node => {
		const tName = protocol.template;
		const prots = this.props.protocols.filter(p => p.template === tName);
		// basically a hashmap of all the different protocols using this template name
		// if theres more than one, theres at least two different protocols using the same template name!
		const diffTemplateMap: {[string]: {name: string, database: string}[]} = {};
		prots.forEach(prot => {
			const p = copyObj(prot);
			const pInfo = {
				name: p.name,
				database: p.database,
			};

			delete p['name'];
			delete p['database'];
			const json = JSON.stringify(p);

			if(Object.keys(diffTemplateMap).includes(json)){
				diffTemplateMap[json].push(pInfo);
			} else {
				diffTemplateMap[json] = [ pInfo ];
			}
		});

		const tMapData = Object.entries(diffTemplateMap);
		if(tMapData.length > 1){
			// theres >=2 protocols using the same template name but with different contents
			// get the currently active protocol
			const prot = copyObj(protocol);
			delete prot['name'];
			delete prot['database'];
			const pStr = JSON.stringify(prot);

			// print out the protocols that are using the same template name but diff contents
			const TemplateAlert = <Alert color='secondary'>
				<p>
					There are protocols using the same template name but with different contents:
				</p>
				<ul>
					{
						tMapData
						// only protocols with different contents
						.filter(([k, pData]) => k !== pStr)
						// get all the protocols of all the different contents
						.reduce((ps, [k, pData]) => [...ps, ...pData], [])
						.map(({ name, database }, i) => <li key={i}>{`${ name } (${ database })`}</li>)
					}
				</ul>
			</Alert>;

			return TemplateAlert;
		} else {
			// valid template useage throughout all dbs
			return null;
		}
	}

	renderProtocol = (index: number, protocol: Protocol) => (
		<Container className='protocol'>
			<FormGroup row>
				<Col sm='2'>
					<Label for='protocolName' className='col-form-label'>Name</Label>
				</Col>
				<Col sm='10'>
					<CacheInput
						name='protocolName'
						placeholder='Protocol name...'
						value={protocol.name}
						onChange={(e) => {
							this.updateProtocol(index, {...protocol, name: e.target.value});
						}}
						fieldTest
					/>
				</Col>
			</FormGroup>
			<FormGroup row>
				<Col sm='2'>
					<Label for='protocolTemplate' className='col-form-label'>Template Name</Label>
				</Col>
				<Col sm='10'>
					<CacheInput
						name='protocolTemplate'
						placeholder='Protocol Template name...'
						value={protocol.template}
						onChange={(e) => {
							this.updateProtocol(index, {...protocol, template: e.target.value});
						}}
						fieldTest
					/>
				</Col>
			</FormGroup>


			{
				// show an alert about protocol template usage
				this.validateProtocolTemplate(protocol)
			}

			<Row className='protocolSets'>
				<Col className='d-flex align-items-center mb-2'>
					<h3 className='mb-0 mr-2'>
						<InfoTooltip
							id='setsTooltip'
							info={`Sets provide different ways to separate the database. For example, one set providing training data and another providing testing data.`}
						>
							Sets
						</InfoTooltip>
					</h3>
					<Button color='success'
						id='newSet'
						onClick={e => {
							const newSets = [...protocol.sets, {
								name: '',
								template: '',
								view: '',
								outputs: {},
								parameters: {},
							}];

							this.updateProtocol(
								index,
								{
									...protocol,
									sets: newSets
								}
							);
						}}
					>
						+
					</Button>
				</Col>

				<Col sm='12'>
					{
						protocol.sets.map((currSet, i) =>
							<Row key={i} className={`set${ i }`}>
								<Col>
									<h4>
										{ currSet.name }
										{' '}
										<ButtonGroup>
											<Button outline color='danger'
												onClick={e => {
													this.updateProtocol(
														index,
														{
															...protocol,
															sets: protocol.sets.filter((s, j) => i !== j)
														}
													);
												}}
											>
												Delete
											</Button>
											<Button outline color='secondary'
												onClick={e => {
													const newSets = [...protocol.sets];
													newSets.splice(i + 1, 0, copyObj(currSet));

													this.updateProtocol(
														index,
														{
															...protocol,
															sets: newSets
														}
													);
												}}
											>
												Clone
											</Button>
										</ButtonGroup>
									</h4>
									<FormGroup row>
										<Col>
											<Label for={`setName${ i }`} className='col-form-label'>Name</Label>
											<CacheInput
												name={`setName${ i }`}
												placeholder='Set name...'
												value={currSet.name}
												onChange={(e) => {
													this.updateSet(index, i, {...currSet, name: e.target.value});
												}}
												fieldTest
											/>
										</Col>
										<Col>
											<Label for={`setTemplate${ i }`} className='col-form-label'>Template Name</Label>
											<CacheInput
												name={`setTemplate${ i }`}
												placeholder='Set Template name...'
												value={currSet.template}
												onChange={(e) => {
													this.updateSet(index, i, {...currSet, template: e.target.value});
												}}
												fieldTest
											/>
										</Col>
										<Col>
											<Label for={`setView${ i }`} className='col-form-label'>
												<InfoTooltip
													id={`view${ i }Tooltip`}
													info={`Every set in a database needs to have a View name assigned to it. This name references a class of the same name in the database's Python file. When data is requested from a set, the View class's code is executed.`}
												>
													View Name
												</InfoTooltip>
											</Label>
											<CacheInput
												name={`setView${ i }`}
												placeholder='Set View name...'
												value={currSet.view}
												onChange={(e) => {
													this.updateSet(index, i, {...currSet, view: e.target.value});
												}}
												fieldTest
											/>
										</Col>
									</FormGroup>
									<Container className='setParameters'>
										<Row>
											<Col sm='12'>
												<div className='d-flex align-items-center mb-2'>
													<h5 className='mb-0 mr-2'>
														<InfoTooltip
															id={`parameters${ i }Tooltip`}
															info={`Parameters are simple values that may be passed to the set's View class' "index" method. This lets a single View class support multiple sets by changing functionality based on these parameters.`}
														>
															Parameters
														</InfoTooltip>
													</h5>
													<Button
														color='success'
														className='newParameter'
														onClick={e => {
															let newKey = '';
															let v = 0;
															while(true) {
																const tryKey = `parameter_${ v }`;
																if(Object.keys(currSet.parameters).includes(tryKey)){
																	v++;
																	continue;
																}
																newKey = tryKey;
																break;
															}

															this.updateSet(index, i, {
																...currSet,
																parameters: {
																	...currSet.parameters,
																	[newKey]: ''
																}
															});
														}}
													>
														+
													</Button>
												</div>
											</Col>
										</Row>
										{
											Object.keys(currSet.parameters).length > 0 &&
												<Row>
													<Col sm='6'>
														<h6>Parameter Name</h6>
													</Col>
													<Col sm='6'>
														<h6>Parameter Value</h6>
													</Col>
												</Row>
										}
										{
											((Object.entries(currSet.parameters): any): [string, string][]).map(([pName, param], j, ps) =>
												<FormGroup row key={j} className={`setParameter${ j }`}>
													<Col sm='6'>
														<CacheInput
															fieldTest
															value={pName}
															onChange={e => {
																const str = e.target.value;
																const newParams = changeObjFieldName(
																	currSet.parameters,
																	pName,
																	str
																);

																this.updateSet(index, i,
																	{
																		...currSet,
																		parameters: newParams
																	}
																);
															}}
														>
															<DeleteInputBtn
																deleteFunc={() => {
																	const newParameters = { ...currSet.parameters };
																	delete newParameters[pName];

																	this.updateSet(index, i,
																		{
																			...currSet,
																			parameters: newParameters
																		}
																	);
																}}
															/>
														</CacheInput>
													</Col>
													<Col sm='6'>
														<CacheInput
															fieldTest
															value={param}
															onChange={e => {
																const str = e.target.value;
																this.updateSet(index, i,
																	{
																		...currSet,
																		parameters: {
																			...currSet.parameters,
																			[pName]: str
																		}
																	}
																);
															}}

														/>
													</Col>
												</FormGroup>
											)
										}
									</Container>
									<Container className='setOutputs'>
										<Row>
											<Col sm='12'>
												<div className='d-flex align-items-center mb-2'>
													<h5 className='mb-0 mr-2'>
														<InfoTooltip
															id={`outputs${ i }Tooltip`}
															info={`Every set in a database needs at least 1 output. These outputs are of a specific type and provide data to algorithm blocks connected to this output.`}
														>
															Outputs
														</InfoTooltip>
													</h5>
													<Button
														color='success'
														className='newOutput'
														onClick={e => {
															let newKey = '';
															let v = 0;
															while(true) {
																const tryKey = `output_${ v }`;
																if(Object.keys(currSet.outputs).includes(tryKey)){
																	v++;
																	continue;
																}
																newKey = tryKey;
																break;
															}

															this.updateSet(index, i, {
																...currSet,
																outputs: {
																	...currSet.outputs,
																	[newKey]: ''
																}
															});
														}}
													>
														+
													</Button>
												</div>
											</Col>
										</Row>
										{
											Object.keys(currSet.outputs).length > 0 &&
												<Row>
													<Col sm='6'>
														<h6>Output Name</h6>
													</Col>
													<Col sm='6'>
														<h6>Output Type</h6>
													</Col>
												</Row>
										}
										{
											((Object.entries(currSet.outputs): any): [string, string][]).map(([outputName, df], j, os) =>
												<TypedField
													className='setOutput'
													key={j}
													name={outputName}
													type={df}
													types={this.props.dataformats.map(d => d.name).filter(d => !['object', 'array'].includes(d))}
													existingFields={Object.keys(currSet.outputs)}
													nameUpdateFunc={str => {
														const newOutputs = changeObjFieldName(
															currSet.outputs,
															outputName,
															str
														);

														this.updateSet(index, i,
															{
																...currSet,
																outputs: newOutputs
															}
														);
													}}
													typeUpdateFunc={str => this.updateSet(index, i,
														{
															...currSet,
															outputs: {
																...currSet.outputs,
																[outputName]: str
															}
														}
													)}
													deleteFunc={() => {
														const newOutputs = { ...currSet.outputs };
														delete newOutputs[outputName];

														this.updateSet(index, i,
															{
																...currSet,
																outputs: newOutputs
															}
														);
													}}

												/>
											)
										}
										<FormGroup row>
											<Col sm='12'>
											</Col>
										</FormGroup>
									</Container>
								</Col>
							</Row>
						)
					}
				</Col>
			</Row>
		</Container>
	);

	render = () => {
		const protocolIdx = this.state.activeProtocol;
		const protocol = protocolIdx > -1 ? this.props.data.contents.protocols[protocolIdx] || undefined : undefined;

		return (
			<div className='databaseEditor'>
				<div className='d-flex'>
					<Button
						className='mx-auto'
						outline
						color='secondary'
						onClick={() => this.props.saveFunc(this.props.data)}
					>
						Save Changes (Changes are <ValidSchemaBadge entity='database' obj={this.props.data} />)
					</Button>
					<TemplateButton
						data={this.props.data}
						entity={'database'}
					/>
				</div>
				<Form onSubmit={(e) => e.preventDefault()}>
					<FormGroup tag='fieldset'>
						<FormGroup>
							<Label for='description'>Short Description</Label>
							<Input
								type='text'
								name='description'
								placeholder='Database description...'
								value={this.props.data.contents['description']}
								onChange={e => this.setContents({ ...this.props.data.contents, 'description': e.target.value})}
							/>
						</FormGroup>
						<FormGroup>
							<Label for='rootFolder'>Root Folder</Label>
							<Input
								type='text'
								name='rootFolder'
								placeholder='Valid system path...'
								value={this.props.data.contents['root_folder']}
								onChange={e => this.setContents({ ...this.props.data.contents, 'root_folder': e.target.value})}
							/>
						</FormGroup>
					</FormGroup>
					<div className='d-flex align-items-center mb-3'>
						<h3 className='mr-2 mb-0'>
							<InfoTooltip
								id='protocolsTooltip'
								info={`Protocols provide different use-cases for the database.`}
							>
								Protocols
							</InfoTooltip>
						</h3>
						<UncontrolledDropdown className='mr-3'>
							<DropdownToggle color='secondary' caret className=''>
								Active Protocol: { protocol ? protocol.name : '' }
							</DropdownToggle>
							<DropdownMenu>
								{
									this.props.data.contents.protocols
									.map((p, i) =>
										<DropdownItem
											id={`protocolChoice${ i }`}
											key={i}
											onClick={() => this.activateProtocol(i)}
										>
											{ this.props.data.contents.protocols[i].name }{' '}
											({ this.props.data.contents.protocols[i].template })
										</DropdownItem>
									)
								}
							</DropdownMenu>
						</UncontrolledDropdown>
						<ButtonGroup>
							<Button outline color='danger'
								onClick={e => {
									const protocols = this.props.data.contents.protocols
									.filter((p, i) => i !== protocolIdx);

									this.setContents({
										...this.props.data.contents,
										protocols
									});

									this.activateProtocol(protocolIdx === 0 ? 0 : protocolIdx - 1);
								}}
							>
								Delete
							</Button>
							<Button outline color='secondary'
								onClick={e => {
									const protocols = [...this.props.data.contents.protocols];
									protocols.splice(
										protocolIdx + 1,
										0,
										copyObj(protocol)
									);

									this.setContents({
										...this.props.data.contents,
										protocols
									});

									this.activateProtocol(protocolIdx + 1);
								}}
							>
								Clone
							</Button>
							<Button
								outline
								color='success'
								id='newProtocol'
								onClick={e => {
									const protocols = [...this.props.data.contents.protocols, {
										name: '',
										template: '',
										sets: [],
									}];

									this.setContents({
										...this.props.data.contents,
										protocols
									});

									this.activateProtocol(protocols.length - 1);
								}}
							>
								Create
							</Button>
						</ButtonGroup>
						<Dropdown
							isOpen={this.state.insertProtocolOpen}
							toggle={e => this.toggleInsertProtocol(false)}
							style={{flex: 1}}
							className='ml-2 mr-2'
						>
							<DropdownToggle
								tag='span'
							>
								<Input
									onChange={e => {
										this.protocolSearch(e.target.value);
									}}
									id='insertProtocolSearch'
									placeholder={`Insert protocol from template...`}
									title={`You may insert a protocol from a template from any database`}
								/>
							</DropdownToggle>
							<DropdownMenu>
								{
									this.state.searchResults.map((r, i) =>
										<DropdownItem
											tag={Button}
											key={i}
											className='insertProtocolOption'
											onClick={e => {
												const newProtocol = copyObj(r);
												delete newProtocol['database'];

												const newProtocols = [...this.props.data.contents.protocols, newProtocol];
												this.setContents({
													...this.props.data.contents,
													protocols: newProtocols,
												});

												this.activateProtocol(newProtocols.length - 1);
											}}
										>
											{ `${ r.template } (${ r.database })` }
											{' '}
											<small className='text-muted'>
												Sets: { r.sets.length }
											</small>
										</DropdownItem>
									)
								}
							</DropdownMenu>
						</Dropdown>
					</div>
					{ protocol && this.renderProtocol(
						protocolIdx,
						protocol
					)}
				</Form>
				<div className='toc'>
					<h4>Quick Jump:</h4>
					<ListGroup>
						{
							this.props.data.contents.protocols.map((p, i) => {
								const setList = (
									<ListGroup className='mt-1'>
										{
											p.sets.map((s, j) => {
												return (
													<ListGroupItem
														key={j}
													>
														<Button
															color='link'
															size='sm'
															onClick={e => {
																const setEl = document.querySelector(`.set${ j }`);
																if(!setEl)
																	return;
																setEl.scrollIntoView(true);
															}}
														>
															{ s.name.length > 20 ? `${ s.name.slice(0, 20) }…` : s.name }
															{' '}
														</Button>
														<ButtonGroup className='float-right'>
															<Button size='sm' outline color='danger'
																onClick={e => {
																	this.updateProtocol(
																		i,
																		{
																			...p,
																			sets: p.sets.filter((s, k) => j !== k)
																		}
																	);
																}}
															>
																Delete
															</Button>
															<Button size='sm' outline color='secondary'
																onClick={e => {
																	const newSets = [...p.sets];
																	newSets.splice(j + 1, 0, copyObj(s));

																	this.updateProtocol(
																		i,
																		{
																			...p,
																			sets: newSets
																		}
																	);
																}}
															>
																Clone
															</Button>
														</ButtonGroup>
													</ListGroupItem>
												);
											})
										}
										<ListGroupItem tag='button' action className='text-primary'
											onClick={e => {
												const newSets = [...p.sets, {
													name: '',
													template: '',
													view: '',
													outputs: {},
													parameters: {},
												}];

												this.updateProtocol(
													i,
													{
														...p,
														sets: newSets
													}
												);
											}}
										>
											New Set
										</ListGroupItem>
									</ListGroup>
								);

								return (
									<ListGroupItem
										key={i}
									>
										<div
											className='clearfix'
										>
											<Button
												active={i === protocolIdx}
												color='link'
												size='sm'
												onClick={() => i !== protocolIdx && this.activateProtocol(i)}
												title={p.name}
											>
												{ p.name.length > 25 ? `${ p.name.slice(0, 25) }…` : p.name }
											</Button>
											<ButtonGroup className='float-right'>
												<Button size='sm' outline color='danger'
													onClick={e => {
														const protocols = this.props.data.contents.protocols
														.filter((p, i) => i !== protocolIdx);

														this.setContents({
															...this.props.data.contents,
															protocols
														});

														this.activateProtocol(protocolIdx === 0 ? 0 : protocolIdx - 1);
													}}
												>
													Delete
												</Button>
												<Button size='sm' outline color='secondary'
													onClick={e => {
														const protocols = [...this.props.data.contents.protocols];
														protocols.splice(
															protocolIdx + 1,
															0,
															copyObj(protocol)
														);

														this.setContents({
															...this.props.data.contents,
															protocols
														});

														this.activateProtocol(protocolIdx + 1);
													}}
												>
													Clone
												</Button>
											</ButtonGroup>
										</div>
										{ i === protocolIdx && setList }
									</ListGroupItem>
								);
							})
						}
						<ListGroupItem tag='button' action className='text-primary'
							onClick={e => {
								const protocols = [...this.props.data.contents.protocols, {
									name: '',
									template: '',
									sets: [],
								}];

								this.setContents({
									...this.props.data.contents,
									protocols
								});

								this.activateProtocol(protocols.length - 1);
							}}
						>
							New Protocol
						</ListGroupItem>
					</ListGroup>
				</div>
			</div>
		);
	}
}

const mapStateToProps = (state, ownProps) => {
	const dbs = Selectors.databaseGet(state);
	const obj = {
		databases: dbs,
		dataformats: Selectors.dataformatGet(state),
		data: dbs[ownProps.index] || getValidObj(),
		protocols: Selectors.databaseProtocols(state),
	};
	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[`databaseUpdate`](obj.name, obj));
	},
});

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