// @flow
import * as React from 'react';
import {
	Container,
	Row,
	Col,
	TabContent, TabPane,
	Alert,
	Badge,
	Modal, ModalBody, ModalHeader, ModalFooter,
	Table,
	Input,
	Button,
	ButtonGroup,
	FormGroup,
	ListGroup,
	ListGroupItem,
} from 'reactstrap';

import type { FlattenedDatabaseEntry } from '@store/selectors';

import type { ConnectionType } from '@helpers/toolchainTypes';
import type { Protocol, Set as ProtocolSet } from '../database/DatabaseEditor.jsx';
import type { BeatObject } from '@helpers/beat';
import { generateNewKey } from '@helpers';

import SearchWorker from '@helpers/search.worker.js';

type ObjectTypeOption = 'toolchains' | 'protocols' | 'sets' | 'normalAlgorithms' | 'analyzerAlgorithms';

type Props = {
	// func to close modal
	toggle: () => any,
	// is the modal active?
	active: boolean,
	// user selected a search result
	addNewBlocks: (type: ObjectTypeOption, newBlocksData: any[], newConnections?: ConnectionType[]) => any,
	// all toolchains
	toolchains: BeatObject[],
	// all database protocols
	protocols: BeatObject[],
	// sets of protocols of protocols
	sets: FlattenedDatabaseEntry[],
	// algorithms for normal blocks (inputs & outputs)
	normalAlgorithms: BeatObject[],
	// algorithms for analyzer blocks (inputs & results)
	analyzerAlgorithms: BeatObject[],
	usedNames: string[],
};

type Res = BeatObject[] | FlattenedDatabaseEntry[] | any;

type State = {
	searchResults: ?Res,
	searchFilter: ObjectTypeOption,
};

const options: ObjectTypeOption[] = ['toolchains', 'protocols', 'sets', 'normalAlgorithms', 'analyzerAlgorithms'];
const optionText = {
	toolchains: 'Toolchain',
	protocols: 'Protocol',
	sets: 'Dataset',
	normalAlgorithms: 'Block Algorithm',
	analyzerAlgorithms: 'Analyzer Algorithm',
};

class InsertObjectModal extends React.PureComponent<Props, State> {
	constructor(props: Props){
		super(props);
	}

	state = {
		searchResults: this.props[options[0]],
		searchFilter: options[0],
	}

	searchWorker = null;

	componentDidMount = () => {
		this.searchWorker = new SearchWorker();
		this.searchWorker.onmessage = (m) => {
			this.setState(prevState => ({ searchResults: m.data }));
		};
		this.updateFuseInstance();
	}

	componentDidUpdate = (prevProps: Props) => {
		if(this.props[this.state.searchFilter] !== prevProps[this.state.searchFilter]){
			this.updateFuseInstance(this.props[this.state.searchFilter]);
		}
	}

	updateFuseInstance = (data: any[] = this.props[this.state.searchFilter]) => {
		if(!this.searchWorker)
			throw new Error(`searchWorker is not instantiated!`);

		this.searchWorker.postMessage(
			{
				type: 'dataChanged',
				payload: {
					data,
					options: {
						shouldSort: true,
						threshold: 0.6,
						location: 0,
						distance: 100,
						maxPatternLength: 32,
						minMatchCharLength: 1,
						keys: Object.keys(data.length > 0 ? data[0] : {}),
					}
				}
			}
		);
	}

	search = (str: string) => {
		if(!this.searchWorker)
			throw new Error(`searchWorker is not instantiated!`);
		this.searchWorker.postMessage(
			{
				type: 'search',
				payload: str
			}
		);
	}

	updateSearchFilter = (filter: ObjectTypeOption) => {
		this.setState(prevState => ({
			searchFilter: filter,
			searchResults:this.props[filter],
		}));
		this.updateFuseInstance(this.props[filter]);
	}

	stringifyResult = (obj: any, type: ObjectTypeOption): string => {
		if(!obj)
			return '';
		switch(type){
			case 'toolchains':
				// tc obj
				return `datasets: ${ obj.contents.datasets.length }, blocks: ${ obj.contents.blocks.length }`;
			case 'protocols':
				// protocol obj w "database" field for db name
				return `sets: ${ obj.sets.length }`;
			case 'sets':
				// set w protocol & db
				return `outputs: ${ Object.keys(obj.outputs).length }`;
			case 'normalAlgorithms':
				// alg obj
				return `inputs: ${
					obj.contents.groups.reduce((iNum, g) => g.inputs ? iNum + Object.keys(g.inputs).length : iNum, 0)
				}, outputs: ${
					obj.contents.groups.reduce((iNum, g) => g.outputs ? iNum + Object.keys(g.outputs).length : iNum, 0)
				}`;
			case 'analyzerAlgorithms':
				// alg obj
				return `inputs: ${
					obj.contents.groups.reduce((iNum, g) => g.inputs ? iNum + Object.keys(g.inputs).length : iNum, 0)
				}`;
			default:
				return obj.name;
		}
	}

	stringifyResultName = (obj: any) => {
		if(!obj)
			return '';
		if(obj.set)
			return `${ obj.protocol }/${ obj.set } (${ obj.database })`;
		if(obj.database)
			return `${ obj.name } (${ obj.database })`;
		if(obj.name)
			return obj.name;
		return 'undefined';
	}

	generateNewBlockData = (obj: any): {blocks: any[], connections: ConnectionType[]} | any[] => {
		const copiedObj = JSON.parse(JSON.stringify(obj));
		const usedNames = this.props.usedNames;
		const type = this.state.searchFilter;
		switch(type){
			case 'toolchains':
				const rep = copiedObj.contents.representation.blocks;
				// add blocks/connections from a toolchain
				// map all the blocks to new data to give to addBlocks()
				const newBlocksData = [
					...copiedObj.contents.datasets
					.map((blk) => ([generateNewKey(blk.name, usedNames), 'datasets', rep[blk.name].col, rep[blk.name].row, blk])),
					...copiedObj.contents.blocks
					.map((blk) => ([generateNewKey(blk.name, usedNames), 'blocks', rep[blk.name].col, rep[blk.name].row, blk])),
					...copiedObj.contents.analyzers
					.map((blk) => ([generateNewKey(blk.name, usedNames), 'analyzers', rep[blk.name].col, rep[blk.name].row, blk])),
				];

				const nameMap = {};
				for(const b of newBlocksData){
					nameMap[b[4].name] = b[0];
					if(b[1] === 'blocks' || b[1] === 'analyzers')
						b[4].synchronized_channel = nameMap[b[4].synchronized_channel];
				}

				const connections = copiedObj.contents.connections
				.map(({ from, to, channel }) => {
					const fromSplit = from.split('.');
					const toSplit = to.split('.');
					return {
						from: `${ nameMap[fromSplit[0]] }.${ fromSplit[1] }`,
						to: `${ nameMap[toSplit[0]] }.${ toSplit[1] }`,
						channel: nameMap[channel],
					};
				});

				// reset the locations of all these blocks - make sure that the point (0,0) is at the top-left of the toolchain
				const minX = Math.min(...newBlocksData.map(d => d[2]));
				const minY = Math.min(...newBlocksData.map(d => d[3]));
				for(const b of newBlocksData){
					b[2] -= minX;
					b[3] -= minY;
					b[4].name = b[0];
				}

				return {
					blocks: newBlocksData,
					connections,
				};
			case 'protocols':
				// add a block for each set in each protocol in the database
				const newBlocks = obj.sets
				// compile basic info for the set (the new name & the outputs for the block)
				.map((set, i) => ({
					name: generateNewKey(`${ obj.name }_${ set.name }`, usedNames),
					outputs: Object.keys(set.outputs),
				}))
				// convert the set's basic info into data arrs for addBlocks()
				.map((setData, i) => [setData.name, 'datasets', 0, 0, { outputs: setData.outputs }]);

				// shift the y coord of each new block down according to the # of outputs
				// accumulate this change so the blocks will all be x-aligned with 1 row between them
				let yCount = 0;
				for(const i in newBlocks){
					newBlocks[i][3] = yCount;
					yCount += newBlocks[i][4].outputs.length + 3;
				}

				return newBlocks;
			case 'sets':
				// add from database's protocol's set
				const setBlock = [
					generateNewKey(`${ obj.protocol }_${ obj.set }`, usedNames),
					'datasets',
					0,
					0,
					{ outputs: Object.keys(obj.outputs) }
				];

				return [setBlock];
			case 'normalAlgorithms':
				// create a block compatible with the alg
				const normalBlock = [
					generateNewKey(obj.name.replace(/\//g, '_'), usedNames),
					'blocks',
					0,
					0,
					{
						outputs: Object.keys(obj.contents.groups[0].outputs),
						inputs: obj.contents.groups
						.map(g => Object.keys(g.inputs))
						.reduce((arr, is) => [...arr, ...is], []),
						synchronized_channel: '',
					}
				];

				return [normalBlock];
			case 'analyzerAlgorithms':
				// create a block compatible with the alg
				const analyzerBlock = [
					generateNewKey(obj.name.replace(/\//g, '_'), usedNames),
					'analyzers',
					0,
					0,
					{
						inputs: obj.contents.groups
						.map(g => Object.keys(g.inputs))
						.reduce((arr, is) => [...arr, ...is], []),
						synchronized_channel: '',
					}
				];

				return [analyzerBlock];
			default:
				return [];
		}
	}

	render = () => {
		return (
			<Modal
				size='lg'
				isOpen={this.props.active}
				toggle={this.props.toggle}
				fade={true}
				onOpened={e => {
					/*
					const f = document.querySelector('#tcModalInitFocus');
					if(f && f.focus)
						f.focus();
						*/
				}}
			>
				<ModalHeader toggle={this.props.toggle}>
					Insert blocks from an existing object
				</ModalHeader>
				<ModalBody>
					<FormGroup row>
						<Col>
							<span className='text-muted'>
								Insert the blocks from a compatible BEAT object into this toolchain. You may insert an existing toolchain or blocks generated from a database protocol, database protocol set, or algorithm.
							</span>
						</Col>
					</FormGroup>
					<FormGroup row>
						<Col>
						</Col>
						<Col sm='auto'>
							<ButtonGroup>
								{
									options.map((o, i) => {
										const isCurrentOption = this.state.searchFilter === o;
										return (
											<Button
												key={i}
												onClick={e => this.updateSearchFilter(o)}
												color={isCurrentOption ? 'primary' : 'secondary'}
											>
												{ optionText[o] }
											</Button>
										);
									})
								}

							</ButtonGroup>
						</Col>
						<Col>
						</Col>
					</FormGroup>
					<FormGroup row>
						<Col sm={12}>
							<Input
								type='text'
								placeholder='Search...'
								onChange={e => this.search(e.target.value)}
								autoFocus
							/>
						</Col>
					</FormGroup>
					<FormGroup row>
						<Col sm={12}>
							<ListGroup className='searchResults'>
								{
									this.state.searchResults &&
										this.state.searchResults.map((res, i) =>
											<ListGroupItem
												key={i}
												tag='button'
												action
												onClick={e => {
													const newData = this.generateNewBlockData(res);
													if(Array.isArray(newData))
														this.props.addNewBlocks(
															this.state.searchFilter,
															newData,
														);
													else
														this.props.addNewBlocks(
															this.state.searchFilter,
															newData.blocks,
															newData.connections,
														);
													this.props.toggle();
												}}
											>
												{ this.stringifyResultName(res) }
												{' '}
												<small className='text-muted'>
													{ this.stringifyResult(res, this.state.searchFilter) }
												</small>
											</ListGroupItem>
										)
								}
							</ListGroup>
						</Col>
					</FormGroup>
				</ModalBody>
			</Modal>
		);
	}
}

export default InsertObjectModal;
