// @flow
import * as React from 'react';
import {
	Container,
	Row,
	Col,
	Button,
	ButtonGroup,
	Form,
	FormGroup,
	Label,
	Input,
	InputGroup,
	FormText,
	Collapse,
	Card,
	CardHeader,
	CardBody,
	TabContent, TabPane,
	Navbar,
	Nav, NavItem, NavLink, CardTitle, CardText,
	FormFeedback,
	Alert,
	InputGroupAddon,
	Badge,
} from 'reactstrap';
import { ContextMenuTrigger } from 'react-contextmenu';
import cn from 'classnames';
import type { BlockSet, BlockCoords } from '@helpers/toolchainTypes';

export type Props = {
	name: string,
	// provides the location & dimensions of the svg elements of the block
	// (the block itself & its inputs/outputs)
	locMap: {
		block: BlockCoords,
		inputs?: { [string]: BlockCoords },
		outputs?: { [string]: BlockCoords },
	},
	// the block data (name, i/os, channel, etc.)
	data: any,
	// the set the block belongs to ('blocks', 'datasets', or 'analyzers')
	set: BlockSet,
	// handler for the user clicking the block
	handleClick: (name: string, set: BlockSet) => any,
	// handler for the user clicking the block
	handleShiftClick: (name: string, set: BlockSet) => any,
	// the color of the channel the block is sync'd to
	channelColor: string,
	// highlighted blocks have a dashed border (see the 'highlighted' CSS class styling in GraphicalEditor.css)
	highlighted: boolean,
	// string to show at the bottom of the block
	// used to display the selected dataset/algorithm in the experiment editor
	experimentString?: string,
	errors: string[],
	handleContextMenu?: (e: any, name: string, set: string) => any,
};

export const blockNameToId = (name: string) => `block_${ name }`;

class ToolchainBlock extends React.Component<Props> {
	constructor(props: Props){
		super(props);
	}

	/*
	 * Custom implementation of shouldComponentUpdate for performance reasons!
	 * "Why not just use PureComponent??"
	 * PureComponent implements a default shouldComponentUpdate that shallowly compares
	 * all current & next props/state values.
	 * Because the click handlers are always not equal (since they're dynamically generated in the loop in
	 * GraphicalEditor.jsx), blocks are *always* rerendered.
	 * Also, the locMap object of every block is technically somehow a different object whenever
	 * a subset of the blocks are moved.
	 * This means that every block change means rerendering all the blocks.
	 * By writing a custom shouldComponentUpdate, we can use more detailed logic that prevents
	 * the unnecessary rerenders.
	 * A couple things to keep in mind:
	 * - the click handlers dont functionally change if the block data doesnt change.
	 * - the locMap's inputs&outputs values dont change if the block's locMap or data hasnt changed
	 * - the errors array is a different array every time technically, so check the values
	 */
	shouldComponentUpdate(nextProps: Props){
		for(const key in nextProps){
			if(key === 'locMap' || key === 'handleClick' || key === 'handleShiftClick' || key === 'handleContextMenu'){
				continue;
			} else if(key === 'errors'){
				if(nextProps[key].length !== this.props.errors.length)
					return true;
				for(const i in nextProps[key]){
					if(nextProps[key][i] !== this.props[key][i])
						return true;
				}
			} else if(nextProps[key] !== this.props[key]){
				return true;
			}
		}

		const nowLoc = this.props.locMap.block;
		const nextLoc = nextProps.locMap.block;
		if(nowLoc.x !== nextLoc.x)
			return true;
		if(nowLoc.y !== nextLoc.y)
			return true;
		if(nowLoc.width !== nextLoc.width)
			return true;
		if(nowLoc.height !== nextLoc.height)
			return true;

		return false;
	}

	render = () => {
		const name = this.props.name;
		const {x, y, height, width} = this.props.locMap.block;

		const hasInputs = this.props.data && this.props.data.hasOwnProperty('inputs');
		const hasOutputs = this.props.data && this.props.data.hasOwnProperty('outputs');
		const inputs = hasInputs ? this.props.data.inputs : [];
		const outputs = hasOutputs ? this.props.data.outputs : [];
		const nameText = name.length > 17 ? `${ name.slice(0, 17) }…` : name;
		const expStr = this.props.experimentString;

		return (
			<g className='tcBlock'>
				{/* white fill background */}
				<rect
					className='tcBlockWhiteFill'
					x={x} y={y} height={height} width={width}
					fill='white'
				/>
				{/* color background tint */}
				<rect className={cn('tcBlockBackground', {highlighted: this.props.highlighted})}
					x={x} y={y} height={height} width={width}
					fill={this.props.channelColor}
					fillOpacity={0.05}
					stroke='black'
				/>
				{/* block name */}
				<text
					x={x}
					y={y + 16}
					width={width}
					className='tcBlockTitle'
				>
					{ nameText }
				</text>
				{/* line below name */}
				<line
					x1={x}
					y1={y + 20}
					x2={x + width}
					y2={y + 20}
					stroke='grey'
				/>
				{/* the little upper-right block showing the color of the currently sync'd channel */}
				<rect
					x={x + width - 20}
					y={y}
					width={20}
					height={20}
					fill={this.props.channelColor}
				/>
				{/* exp string */}
				{ expStr &&
						<text
							x={x}
							y={y + 36}
							fill='slategray'
						>
							{ expStr.length > 19 ? `${ expStr.slice(0, 19) }…` : expStr }
						</text>
				}

				{
					/* inputs
					 * draws the inputs (both the black rects for connections and the corresponding input name)
					 */
					inputs.map((inp, i) => {
						// get the location/dimensions of the input
						const loc = this.props.locMap.inputs[inp];
						// width changes between half the block and the full block depending if theres outputs or not
						const varWidth = hasOutputs ? width / 2 : width;
						// the shown text needs to be truncated based on the available space
						// easy to calculate because the text is monospace!
						const maxlen = hasOutputs ? 8 : 19;
						// truncate text, add ellipsis if it's too long
						const text = inp.length > maxlen ? `${ inp.slice(0, maxlen) }…` : inp;
						return (
							<g
								key={i}
								x={loc.x}
								y={loc.y}
							>
								<rect
									className='iBlock'
									id={`${ name }-input-${ inp }`}
									key={i}
									{...loc}
									fill='black'
								/>
								<text
									x={loc.x + loc.width}
									y={loc.y + loc.height * 0.75}
									width={varWidth}
								>
									{ text }
								</text>
							</g>
						);
					})
				}
				{
					/* outputs
					 * draws the inputs (both the black rects for connections and the corresponding input name)
					 */
					outputs.map((out, i) => {
						const loc = this.props.locMap.outputs[out];
						// width changes between half the block and the full block depending if theres inputs or not
						const varWidth = hasInputs ? width / 2 : width;
						// the shown text needs to be truncated based on the available space
						// easy to calculate because the text is monospace!
						const maxlen = hasInputs ? 9 : 19;
						// truncate text, add ellipsis if it's too long
						const text = out.length > maxlen ? `${ out.slice(0, maxlen) }…` : out;
						// to align output text to the output blocks (the right side of the block),
						// calculate the x-offset needed, in character count
						const offsetCharacterCount = (hasInputs ? 10 : 20) - text.length;
						// a monospace char @font-size=16px is apparently 10px wide
						const charWidthPx = 10;
						// put it all together, offset 1px to the left to avoid chars overlapping with block border
						const x = ((loc.x - varWidth) + offsetCharacterCount * charWidthPx) - 1;
						return (
							<g
								key={i}
							>
								<rect
									className='oBlock'
									id={`${ name }-output-${ out }`}
									key={i}
									{...loc}
									fill='black'
								/>
								<text
									x={x}
									y={loc.y + loc.height * 0.75}
								>
									{ text }
								</text>
							</g>
						);
					})
				}
				{/* block errors for the experiment editor */}
				{ this.props.errors.length > 0 &&
						<React.Fragment>
							<rect className='errors'
								x={x} y={y} height={height} width={width}
								fill='crimson'
								opacity='0.8'
								stroke='crimson'
							/>
							<text
								x={x + width / 2 - 5}
								y={y + 40}
								fontSize={50}
								fill='white'
							>
								!
							</text>
							<text
								x={x + 2}
								y={y + height - 6}
								fill='white'
								fontSize={12}
							>
								{ this.props.errors[0] }
							</text>
						</React.Fragment>
				}
				{/* the background rectangle with the click handler */}
				<rect id={blockNameToId(name)} className='fo'
					x={x} y={y} height={height} width={width}
					fill='white'
					opacity={0.0001}
					onClick={e => {
						if(!(e.button === 0 || e.button === undefined))
							return;
						if(e.shiftKey)
							this.props.handleShiftClick(name, this.props.set);
						else
							this.props.handleClick(name, this.props.set);
						e.stopPropagation();
						e.preventDefault();
					}}
					onContextMenu={e => this.props.handleContextMenu && this.props.handleContextMenu(e, this.props.name, this.props.set)}
				>
					<title>
						{ name } synchronized to the {this.props.data.synchronized_channel || name} channel
						{ this.props.experimentString ? ` with "${ this.props.experimentString }" assigned to it` : ''}
					</title>
				</rect>
			</g>
		);
	}
}

export default ToolchainBlock;
